Skip to content

Commit 983a12c

Browse files
committed
Fix writing to streams from profile.ps1 (#411)
* Refactor PowerShellManagerPool to make it more testable * Store _createLogger instead of _msgStream in PowerShellManagerPool * Pass Func<ILogger> to PowerShellManagerPool ctor * Pass requestId and invocationId instead of StreamingMessage to CheckoutIdleWorker * Pass functionName to CheckoutIdleWorker * Pass outputBindings instead of AzFunctionInfo to RegisterFunctionMetadata * Pass outputBindings to CheckoutIdleWorker * Don't pass AzFunctionInfo to CheckoutIdleWorker * Add LoggerContextIsSet test * Set invocation context on the logger before invoking profile.ps1 * Improve the message on errors reported by profile * Get rid of 'Import-Module profile' output
1 parent 72cfd4c commit 983a12c

File tree

7 files changed

+84
-34
lines changed

7 files changed

+84
-34
lines changed

src/PowerShell/PowerShellManager.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ internal void InvokeProfile(string profilePath)
162162
return;
163163
}
164164

165+
var profileExecutionHadErrors = false;
166+
165167
try
166168
{
167169
var stopwatch = new Stopwatch();
@@ -170,8 +172,13 @@ internal void InvokeProfile(string profilePath)
170172
// Import-Module on a .ps1 file will evaluate the script in the global scope.
171173
_pwsh.AddCommand(Utils.ImportModuleCmdletInfo)
172174
.AddParameter("Name", profilePath)
173-
.AddParameter("PassThru", Utils.BoxedTrue)
174-
.AddCommand(Utils.RemoveModuleCmdletInfo)
175+
.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject")
176+
.InvokeAndClearCommands();
177+
178+
profileExecutionHadErrors = _pwsh.HadErrors;
179+
180+
_pwsh.AddCommand(Utils.RemoveModuleCmdletInfo)
181+
.AddParameter("FullyQualifiedName", profilePath)
175182
.AddParameter("Force", Utils.BoxedTrue)
176183
.AddParameter("ErrorAction", "SilentlyContinue")
177184
.InvokeAndClearCommands();
@@ -185,9 +192,9 @@ internal void InvokeProfile(string profilePath)
185192
}
186193
finally
187194
{
188-
if (_pwsh.HadErrors)
195+
if (profileExecutionHadErrors || _pwsh.HadErrors)
189196
{
190-
string errorMsg = string.Format(PowerShellWorkerStrings.FailToRunProfile, profilePath);
197+
string errorMsg = string.Format(PowerShellWorkerStrings.ErrorsWhileExecutingProfile, profilePath);
191198
Logger.Log(isUserOnlyLog: true, LogLevel.Error, errorMsg, exception);
192199
}
193200
}

src/PowerShell/PowerShellManagerPool.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6+
using System;
67
using System.Collections.Concurrent;
8+
using System.Collections.ObjectModel;
79
using System.Threading;
8-
using Microsoft.Azure.Functions.PowerShellWorker.Messaging;
910
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
10-
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
1111

1212
namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell
1313
{
@@ -19,7 +19,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell
1919
/// </summary>
2020
internal class PowerShellManagerPool
2121
{
22-
private readonly MessagingStream _msgStream;
22+
private readonly Func<ILogger> _createLogger;
2323
private readonly BlockingCollection<PowerShellManager> _pool;
2424
private int _poolSize;
2525

@@ -31,9 +31,9 @@ internal class PowerShellManagerPool
3131
/// <summary>
3232
/// Constructor of the pool.
3333
/// </summary>
34-
internal PowerShellManagerPool(MessagingStream msgStream)
34+
internal PowerShellManagerPool(Func<ILogger> createLogger)
3535
{
36-
_msgStream = msgStream;
36+
_createLogger = createLogger;
3737
_pool = new BlockingCollection<PowerShellManager>(UpperBound);
3838
RpcLogger.WriteSystemLog(LogLevel.Information, string.Format(PowerShellWorkerStrings.LogConcurrencyUpperBound, UpperBound.ToString()));
3939
}
@@ -44,7 +44,7 @@ internal PowerShellManagerPool(MessagingStream msgStream)
4444
/// </summary>
4545
internal void Initialize(PowerShell pwsh)
4646
{
47-
var logger = new RpcLogger(_msgStream);
47+
var logger = _createLogger();
4848
var psManager = new PowerShellManager(logger, pwsh);
4949
_pool.Add(psManager);
5050
_poolSize = 1;
@@ -53,11 +53,13 @@ internal void Initialize(PowerShell pwsh)
5353
/// <summary>
5454
/// Checkout an idle PowerShellManager instance in a non-blocking asynchronous way.
5555
/// </summary>
56-
internal PowerShellManager CheckoutIdleWorker(StreamingMessage request, AzFunctionInfo functionInfo)
56+
internal PowerShellManager CheckoutIdleWorker(
57+
string requestId,
58+
string invocationId,
59+
string functionName,
60+
ReadOnlyDictionary<string, ReadOnlyBindingInfo> outputBindings)
5761
{
5862
PowerShellManager psManager = null;
59-
string requestId = request.RequestId;
60-
string invocationId = request.InvocationRequest?.InvocationId;
6163

6264
// If the pool has an idle one, just use it.
6365
if (!_pool.TryTake(out psManager))
@@ -80,21 +82,22 @@ internal PowerShellManager CheckoutIdleWorker(StreamingMessage request, AzFuncti
8082
if (psManager == null)
8183
{
8284
var logger = CreateLoggerWithContext(requestId, invocationId);
83-
logger.Log(isUserOnlyLog: true, LogLevel.Warning, string.Format(PowerShellWorkerStrings.FunctionQueuingRequest, functionInfo.FuncName));
85+
logger.Log(isUserOnlyLog: true, LogLevel.Warning, string.Format(PowerShellWorkerStrings.FunctionQueuingRequest, functionName));
8486

8587
// If the pool has reached its bounded capacity, then the thread
8688
// should be blocked until an idle one becomes available.
8789
psManager = _pool.Take();
8890
}
8991
}
9092

93+
psManager.Logger.SetContext(requestId, invocationId);
94+
9195
// Finish the initialization if not yet.
9296
// This applies only to the very first PowerShellManager instance, whose initialization was deferred.
9397
psManager.Initialize();
9498

9599
// Register the function with the Runspace before returning the idle PowerShellManager.
96-
FunctionMetadata.RegisterFunctionMetadata(psManager.InstanceId, functionInfo);
97-
psManager.Logger.SetContext(requestId, invocationId);
100+
FunctionMetadata.RegisterFunctionMetadata(psManager.InstanceId, outputBindings);
98101

99102
return psManager;
100103
}
@@ -113,9 +116,9 @@ internal void ReclaimUsedWorker(PowerShellManager psManager)
113116
}
114117
}
115118

116-
private RpcLogger CreateLoggerWithContext(string requestId, string invocationId)
119+
private ILogger CreateLoggerWithContext(string requestId, string invocationId)
117120
{
118-
var logger = new RpcLogger(_msgStream);
121+
var logger = _createLogger();
119122
logger.SetContext(requestId, invocationId);
120123
return logger;
121124
}

src/Public/FunctionMetadata.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ internal static Hashtable GetOutputBindingHashtable(Guid runspaceInstanceId)
4242
/// <summary>
4343
/// Helper method to set the output binding metadata for the function that is about to run.
4444
/// </summary>
45-
internal static void RegisterFunctionMetadata(Guid instanceId, AzFunctionInfo functionInfo)
45+
internal static void RegisterFunctionMetadata(Guid instanceId, ReadOnlyDictionary<string, ReadOnlyBindingInfo> outputBindings)
4646
{
47-
var outputBindings = functionInfo.OutputBindings;
4847
OutputBindingCache.AddOrUpdate(instanceId, outputBindings, (key, value) => outputBindings);
4948
}
5049

src/RequestProcessor.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal class RequestProcessor
4040
internal RequestProcessor(MessagingStream msgStream)
4141
{
4242
_msgStream = msgStream;
43-
_powershellPool = new PowerShellManagerPool(msgStream);
43+
_powershellPool = new PowerShellManagerPool(() => new RpcLogger(msgStream));
4444

4545
// Host sends capabilities/init data to worker
4646
_requestHandlers.Add(StreamingMessage.ContentOneofCase.WorkerInitRequest, ProcessWorkerInitRequest);
@@ -255,7 +255,13 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
255255
stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.DependenciesAvailable);
256256

257257
AzFunctionInfo functionInfo = FunctionLoader.GetFunctionInfo(request.InvocationRequest.FunctionId);
258-
PowerShellManager psManager = _powershellPool.CheckoutIdleWorker(request, functionInfo);
258+
259+
PowerShellManager psManager = _powershellPool.CheckoutIdleWorker(
260+
request.RequestId,
261+
request.InvocationRequest?.InvocationId,
262+
functionInfo.FuncName,
263+
functionInfo.OutputBindings);
264+
259265
stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.RunspaceAvailable);
260266

261267
// When the concurrency upper bound is more than 1, we have to handle the invocation in a worker

src/resources/PowerShellWorkerStrings.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@
154154
<data name="FileNotFound" xml:space="preserve">
155155
<value>No '{0}' is found at the function app root folder: {1}.</value>
156156
</data>
157-
<data name="FailToRunProfile" xml:space="preserve">
158-
<value>Failed to run profile.ps1. See logs for detailed errors. Profile location: {0}.</value>
157+
<data name="ErrorsWhileExecutingProfile" xml:space="preserve">
158+
<value>Errors reported while executing profile.ps1. See logs for detailed errors. Profile location: {0}.</value>
159159
</data>
160160
<data name="UnsupportedMessage" xml:space="preserve">
161161
<value>Unsupported message type: {0}.</value>

test/Unit/Modules/HelperModuleTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ static HelperModuleTests()
5656

5757
public HelperModuleTests()
5858
{
59-
FunctionMetadata.RegisterFunctionMetadata(s_pwsh.Runspace.InstanceId, s_funcInfo);
59+
FunctionMetadata.RegisterFunctionMetadata(s_pwsh.Runspace.InstanceId, s_funcInfo.OutputBindings);
6060
}
6161

6262
public void Dispose()

test/Unit/PowerShell/PowerShellManagerTests.cs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public void InvokeBasicFunctionWorks()
105105

106106
try
107107
{
108-
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo);
108+
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo.OutputBindings);
109109
Hashtable result = InvokeFunction(testManager, functionInfo);
110110

111111
// The outputBinding hashtable for the runspace should be cleared after 'InvokeFunction'
@@ -130,7 +130,7 @@ public void InvokeFunctionWithSpecialVariableWorks()
130130

131131
try
132132
{
133-
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo);
133+
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo.OutputBindings);
134134
Hashtable result = InvokeFunction(testManager, functionInfo);
135135

136136
// The outputBinding hashtable for the runspace should be cleared after 'InvokeFunction'
@@ -155,7 +155,7 @@ public void InvokeBasicFunctionWithRequiresWorks()
155155

156156
try
157157
{
158-
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo);
158+
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo.OutputBindings);
159159
Hashtable result = InvokeFunction(testManager, functionInfo);
160160

161161
// The outputBinding hashtable for the runspace should be cleared after 'InvokeFunction'
@@ -186,7 +186,7 @@ public void InvokeBasicFunctionWithTriggerMetadataAndTraceContextWorks()
186186

187187
try
188188
{
189-
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo);
189+
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo.OutputBindings);
190190

191191
Hashtable result = InvokeFunction(testManager, functionInfo, triggerMetadata);
192192

@@ -212,7 +212,7 @@ public void InvokeFunctionWithEntryPointWorks()
212212

213213
try
214214
{
215-
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo);
215+
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo.OutputBindings);
216216
Hashtable result = InvokeFunction(testManager, functionInfo);
217217

218218
// The outputBinding hashtable for the runspace should be cleared after 'InvokeFunction'
@@ -236,7 +236,7 @@ public void FunctionShouldCleanupVariableTable()
236236

237237
try
238238
{
239-
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo);
239+
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo.OutputBindings);
240240

241241
Hashtable result1 = InvokeFunction(testManager, functionInfo);
242242
Assert.Equal("is not set", result1[TestOutputBindingName]);
@@ -266,7 +266,7 @@ public void RegisterAndUnregisterFunctionMetadataShouldWork()
266266
string path = Path.Join(s_funcDirectory, "testBasicFunction.ps1");
267267
var (functionInfo, testManager) = PrepareFunction(path, string.Empty);
268268

269-
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo);
269+
FunctionMetadata.RegisterFunctionMetadata(testManager.InstanceId, functionInfo.OutputBindings);
270270
var outBindingMap = FunctionMetadata.GetOutputBindingInfo(testManager.InstanceId);
271271
Assert.Single(outBindingMap);
272272
Assert.Equal(TestOutputBindingName, outBindingMap.First().Key);
@@ -317,7 +317,7 @@ public void ProfileWithTerminatingError()
317317
Assert.Throws<CmdletInvocationException>(() => testManager.InvokeProfile(profilePath));
318318
var relevantLogs = s_testLogger.FullLog.Where(message => !message.StartsWith("Trace:")).ToList();
319319
Assert.Single(relevantLogs);
320-
Assert.Matches("Error: Failed to run profile.ps1. See logs for detailed errors. Profile location: ", relevantLogs[0]);
320+
Assert.Matches("Error: Errors reported while executing profile.ps1. See logs for detailed errors. Profile location: ", relevantLogs[0]);
321321
}
322322

323323
[Fact]
@@ -334,7 +334,7 @@ public void ProfileWithNonTerminatingError()
334334
Assert.Equal(2, relevantLogs.Count);
335335
Assert.StartsWith("Error: ERROR: ", relevantLogs[0]);
336336
Assert.Contains("help me!", relevantLogs[0]);
337-
Assert.Matches("Error: Failed to run profile.ps1. See logs for detailed errors. Profile location: ", relevantLogs[1]);
337+
Assert.Matches("Error: Errors reported while executing profile.ps1. See logs for detailed errors. Profile location: ", relevantLogs[1]);
338338
}
339339

340340
[Fact]
@@ -358,9 +358,44 @@ public void PSManagerCtorDoesNotRunProfileIfDelayInit()
358358
Assert.Empty(s_testLogger.FullLog);
359359
}
360360

361+
[Fact]
362+
public void LoggerContextIsSet()
363+
{
364+
var dummyBindingInfo = new Dictionary<string, ReadOnlyBindingInfo>();
365+
var outputBindings = new ReadOnlyDictionary<string, ReadOnlyBindingInfo>(dummyBindingInfo);
366+
367+
var powerShellManagerPool = new PowerShellManagerPool(() => new ContextValidatingLogger());
368+
var pwsh = Utils.NewPwshInstance();
369+
powerShellManagerPool.Initialize(pwsh);
370+
371+
var worker = powerShellManagerPool.CheckoutIdleWorker("requestId", "invocationId", "FuncName", outputBindings);
372+
373+
powerShellManagerPool.ReclaimUsedWorker(worker);
374+
}
375+
361376
private static Hashtable InvokeFunction(PowerShellManager powerShellManager, AzFunctionInfo functionInfo, Hashtable triggerMetadata = null)
362377
{
363378
return powerShellManager.InvokeFunction(functionInfo, triggerMetadata, null, s_testInputData, new FunctionInvocationPerformanceStopwatch());
364379
}
380+
381+
private class ContextValidatingLogger : ILogger
382+
{
383+
private bool _isContextSet = false;
384+
385+
public void Log(bool isUserOnlyLog, RpcLog.Types.Level logLevel, string message, Exception exception = null)
386+
{
387+
Assert.True(_isContextSet);
388+
}
389+
390+
public void SetContext(string requestId, string invocationId)
391+
{
392+
_isContextSet = true;
393+
}
394+
395+
public void ResetContext()
396+
{
397+
_isContextSet = false;
398+
}
399+
}
365400
}
366401
}

0 commit comments

Comments
 (0)