Skip to content

Commit 91704b2

Browse files
authored
Log a warning when a command is not found or a module is not found (#512)
1 parent 96dcc44 commit 91704b2

File tree

5 files changed

+212
-10
lines changed

5 files changed

+212
-10
lines changed

src/PowerShell/ErrorAnalysisLogger.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 System.Management.Automation;
7+
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
8+
using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level;
9+
10+
namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell
11+
{
12+
internal class ErrorAnalysisLogger
13+
{
14+
public static void Log(ILogger logger, ErrorRecord errorRecord, bool isException)
15+
{
16+
switch (errorRecord.FullyQualifiedErrorId)
17+
{
18+
case KnownErrorId.CommandNotFound:
19+
LogCommandNotFoundWarning(logger, errorRecord, isException);
20+
break;
21+
22+
case KnownErrorId.ModuleNotFound:
23+
LogModuleNotFoundWarning(logger, errorRecord, isException);
24+
break;
25+
}
26+
}
27+
28+
private static void LogCommandNotFoundWarning(ILogger logger, ErrorRecord errorRecord, bool isException)
29+
{
30+
var publicMessage = isException
31+
? PowerShellWorkerStrings.CommandNotFoundException_Exception
32+
: PowerShellWorkerStrings.CommandNotFoundException_Error;
33+
34+
var userMessage = string.Format(
35+
PowerShellWorkerStrings.CommandNotFoundUserWarning,
36+
errorRecord.CategoryInfo.TargetName);
37+
38+
LogWarning(logger, publicMessage, userMessage);
39+
}
40+
41+
private static void LogModuleNotFoundWarning(ILogger logger, ErrorRecord errorRecord, bool isException)
42+
{
43+
var publicMessage = isException
44+
? PowerShellWorkerStrings.ModuleNotFound_Exception
45+
: PowerShellWorkerStrings.ModuleNotFound_Error;
46+
47+
var userMessage = string.Format(
48+
PowerShellWorkerStrings.ModuleNotFoundUserWarning,
49+
errorRecord.CategoryInfo.TargetName);
50+
51+
LogWarning(logger, publicMessage, userMessage);
52+
}
53+
54+
private static void LogWarning(ILogger logger, string publicMessage, string userMessage)
55+
{
56+
logger.Log(isUserOnlyLog: false, LogLevel.Warning, publicMessage);
57+
logger.Log(isUserOnlyLog: true, LogLevel.Warning, userMessage);
58+
}
59+
60+
// These error IDs is what PowerShell currently uses, even though this is not documented nor promised.
61+
// If this ever changes in future, the ErrorAnalysisLogger tests are supposed to catch that,
62+
// and these IDs or the detection logic will have to be updated.
63+
private static class KnownErrorId
64+
{
65+
public const string CommandNotFound = "CommandNotFoundException";
66+
public const string ModuleNotFound = "Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand";
67+
}
68+
}
69+
}

src/PowerShell/PowerShellManager.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,7 @@ public Hashtable InvokeFunction(
229229
}
230230
catch (RuntimeException e)
231231
{
232-
if (e.ErrorRecord.FullyQualifiedErrorId == "CommandNotFoundException")
233-
{
234-
Logger.Log(isUserOnlyLog: false, LogLevel.Warning, PowerShellWorkerStrings.CommandNotFoundException_Exception);
235-
}
236-
232+
ErrorAnalysisLogger.Log(Logger, e.ErrorRecord, isException: true);
237233
Logger.Log(isUserOnlyLog: true, LogLevel.Error, GetFunctionExceptionMessage(e));
238234
throw;
239235
}

src/PowerShell/StreamHandler.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,7 @@ public void ErrorDataAdding(object sender, DataAddingEventArgs e)
3333
{
3434
if(e.ItemAdded is ErrorRecord record)
3535
{
36-
if (record.FullyQualifiedErrorId == "CommandNotFoundException")
37-
{
38-
_logger.Log(isUserOnlyLog: false, LogLevel.Warning, PowerShellWorkerStrings.CommandNotFoundException_Error);
39-
}
40-
36+
ErrorAnalysisLogger.Log(_logger, record, isException: false);
4137
_logger.Log(isUserOnlyLog: true, LogLevel.Error, $"ERROR: {_errorRecordFormatter.Format(record)}", record.Exception);
4238
}
4339
}

src/resources/PowerShellWorkerStrings.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,18 @@
310310
<data name="CommandNotFoundException_Exception" xml:space="preserve">
311311
<value>CommandNotFoundException detected (exception).</value>
312312
</data>
313+
<data name="CommandNotFoundUserWarning" xml:space="preserve">
314+
<value>The Function app may be missing a module containing the '{0}' command definition. If this command belongs to a module available on the PowerShell Gallery, add a reference to this module to requirements.psd1. Make sure this module is compatible with PowerShell 7. For more details, see https://aka.ms/functions-powershell-managed-dependency.</value>
315+
</data>
316+
<data name="ModuleNotFound_Error" xml:space="preserve">
317+
<value>ModuleNotFound detected (error).</value>
318+
</data>
319+
<data name="ModuleNotFound_Exception" xml:space="preserve">
320+
<value>ModuleNotFound detected (exception).</value>
321+
</data>
322+
<data name="ModuleNotFoundUserWarning" xml:space="preserve">
323+
<value>The Function app may be missing the '{0}' module. If '{0}' is available on the PowerShell Gallery, add a reference to this module to requirements.psd1. Make sure this module is compatible with PowerShell 7. For more details, see https://aka.ms/functions-powershell-managed-dependency.</value>
324+
</data>
313325
<data name="PowerShellVersion" xml:space="preserve">
314326
<value>PowerShell version: '{0}'.</value>
315327
</data>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
namespace Microsoft.Azure.Functions.PowerShellWorker.Test
7+
{
8+
using System;
9+
using System.Management.Automation;
10+
11+
using Microsoft.Azure.Functions.PowerShellWorker.PowerShell;
12+
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
13+
using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level;
14+
15+
using Moq;
16+
using Xunit;
17+
18+
public class ErrorAnalysisLoggerTests
19+
{
20+
private readonly Mock<ILogger> _mockLogger = new Mock<ILogger>();
21+
22+
[Theory]
23+
[InlineData(true)]
24+
[InlineData(false)]
25+
public void DoesNotLogUnknownErrors(bool isException)
26+
{
27+
var error = new ErrorRecord(
28+
new Exception(),
29+
"UnknownException",
30+
ErrorCategory.NotSpecified,
31+
"Dummy target object");
32+
33+
ErrorAnalysisLogger.Log(_mockLogger.Object, error, isException);
34+
}
35+
36+
[Theory]
37+
[InlineData(true)]
38+
[InlineData(false)]
39+
public void LogsCommandNotFound(bool isException)
40+
{
41+
const string FakeUnknownCommand = "Unknown-Command";
42+
43+
var error = CreateCommandNotFoundError(FakeUnknownCommand);
44+
45+
ErrorAnalysisLogger.Log(_mockLogger.Object, error, isException);
46+
47+
_mockLogger.Verify(
48+
_ => _.Log(
49+
false,
50+
LogLevel.Warning,
51+
It.Is<string>(
52+
message => message.Contains("CommandNotFoundException")
53+
&& (isException && message.Contains("(exception)") && !message.Contains("(error)")
54+
|| !isException && message.Contains("(error)") && !message.Contains("(exception)"))
55+
&& !message.Contains(FakeUnknownCommand)),
56+
null),
57+
Times.Once);
58+
59+
_mockLogger.Verify(
60+
_ => _.Log(
61+
true,
62+
LogLevel.Warning,
63+
It.Is<string>(message => message.Contains(FakeUnknownCommand)),
64+
null),
65+
Times.Once);
66+
67+
_mockLogger.VerifyNoOtherCalls();
68+
}
69+
70+
[Theory]
71+
[InlineData(true)]
72+
[InlineData(false)]
73+
public void LogsModuleNotFound(bool isException)
74+
{
75+
const string FakeUnknownModule = "UnknownModule";
76+
77+
var error = CreateModuleNotFoundError(FakeUnknownModule);
78+
79+
ErrorAnalysisLogger.Log(_mockLogger.Object, error, isException);
80+
81+
_mockLogger.Verify(
82+
_ => _.Log(
83+
false,
84+
LogLevel.Warning,
85+
It.Is<string>(
86+
message => message.Contains("ModuleNotFound")
87+
&& (isException && message.Contains("(exception)") && !message.Contains("(error)")
88+
|| !isException && message.Contains("(error)") && !message.Contains("(exception)"))
89+
&& !message.Contains(FakeUnknownModule)),
90+
null),
91+
Times.Once);
92+
93+
_mockLogger.Verify(
94+
_ => _.Log(
95+
true,
96+
LogLevel.Warning,
97+
It.Is<string>(message => message.Contains(FakeUnknownModule)),
98+
null),
99+
Times.Once);
100+
101+
_mockLogger.VerifyNoOtherCalls();
102+
}
103+
104+
private static ErrorRecord CreateCommandNotFoundError(string commandName)
105+
{
106+
using var ps = PowerShell.Create();
107+
ps.AddCommand(commandName);
108+
try
109+
{
110+
ps.Invoke();
111+
}
112+
catch (CommandNotFoundException e)
113+
{
114+
return e.ErrorRecord;
115+
}
116+
117+
throw new Exception("Expected CommandNotFoundException is not thrown. Incompatible PowerShell version?");
118+
}
119+
120+
private static ErrorRecord CreateModuleNotFoundError(string moduleName)
121+
{
122+
using var ps = PowerShell.Create();
123+
ps.AddCommand("Import-Module");
124+
ps.AddParameter("Name", moduleName);
125+
ps.Invoke();
126+
return ps.Streams.Error[0];
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)