Skip to content

Commit 3351367

Browse files
rjmholtJamesWTruher
authored andcommitted
Work around runspacepool deadlock (#1316)
* Add test * Partial fix * Implement workaround for PackageManagement cmdlets * Fix formatting on test file, add copyright * Fix net452 issues * Add comments to address @JamesWTruher's feedback
1 parent 8022a18 commit 3351367

File tree

3 files changed

+936
-33
lines changed

3 files changed

+936
-33
lines changed

Rules/UseCmdletCorrectly.cs

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Management.Automation;
88
using System.Management.Automation.Language;
99
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
10+
using System.Collections.Concurrent;
1011
#if !CORECLR
1112
using System.ComponentModel.Composition;
1213
#endif
@@ -22,6 +23,27 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
2223
#endif
2324
public class UseCmdletCorrectly : IScriptRule
2425
{
26+
// Cache of the mandatory parameters of cmdlets in PackageManagement
27+
// Key: Cmdlet name
28+
// Value: List of mandatory parameters
29+
private static readonly ConcurrentDictionary<string, IReadOnlyList<string>> s_pkgMgmtMandatoryParameters =
30+
new ConcurrentDictionary<string, IReadOnlyList<string>>(new Dictionary<string, IReadOnlyList<string>>
31+
{
32+
{ "Find-Package", new string[0] },
33+
{ "Find-PackageProvider", new string[0] },
34+
{ "Get-Package", new string[0] },
35+
{ "Get-PackageProvider", new string[0] },
36+
{ "Get-PackageSource", new string[0] },
37+
{ "Import-PackageProvider", new string[] { "Name" } },
38+
{ "Install-Package", new string[] { "Name" } },
39+
{ "Install-PackageProvider", new string[] { "Name" } },
40+
{ "Register-PackageSource", new string[] { "ProviderName" } },
41+
{ "Save-Package", new string[] { "Name", "InputObject" } },
42+
{ "Set-PackageSource", new string[] { "Name", "Location" } },
43+
{ "Uninstall-Package", new string[] { "Name", "InputObject" } },
44+
{ "Unregister-PackageSource", new string[] { "Name", "InputObject" } },
45+
});
46+
2547
/// <summary>
2648
/// AnalyzeScript: Check that cmdlets are invoked with the correct mandatory parameter
2749
/// </summary>
@@ -61,41 +83,69 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
6183
/// <returns></returns>
6284
private bool MandatoryParameterExists(CommandAst cmdAst)
6385
{
64-
CommandInfo cmdInfo = null;
65-
List<ParameterMetadata> mandParams = new List<ParameterMetadata>();
66-
IEnumerable<CommandElementAst> ceAsts = null;
67-
bool returnValue = false;
86+
#region Compares parameter list and mandatory parameter list.
6887

69-
#region Predicates
88+
CommandInfo cmdInfo = Helper.Instance.GetCommandInfoLegacy(cmdAst.GetCommandName());
7089

71-
// Predicate to find ParameterAsts.
72-
Func<CommandElementAst, bool> foundParamASTs = delegate(CommandElementAst ceAst)
90+
// If we can't resolve the command or it's not a cmdlet, we are done
91+
if (cmdInfo == null || (cmdInfo.CommandType != System.Management.Automation.CommandTypes.Cmdlet))
7392
{
74-
if (ceAst is CommandParameterAst) return true;
75-
return false;
76-
};
77-
78-
#endregion
93+
return true;
94+
}
7995

80-
#region Compares parameter list and mandatory parameter list.
96+
// We can't statically analyze splatted variables, so ignore them
97+
if (Helper.Instance.HasSplattedVariable(cmdAst))
98+
{
99+
return true;
100+
}
81101

82-
cmdInfo = Helper.Instance.GetCommandInfoLegacy(cmdAst.GetCommandName());
83-
if (cmdInfo == null || (cmdInfo.CommandType != System.Management.Automation.CommandTypes.Cmdlet))
102+
// Positional parameters could be mandatory, so we assume all is well
103+
if (Helper.Instance.PositionalParameterUsed(cmdAst) && Helper.Instance.IsKnownCmdletFunctionOrExternalScript(cmdAst))
84104
{
85105
return true;
86106
}
87107

88-
// ignores if splatted variable is used
89-
if (Helper.Instance.HasSplattedVariable(cmdAst))
108+
// If the command is piped to, this also precludes mandatory parameters
109+
if (cmdAst.Parent is PipelineAst parentPipeline
110+
&& parentPipeline.PipelineElements.Count > 1
111+
&& parentPipeline.PipelineElements[0] != cmdAst)
90112
{
91113
return true;
92114
}
93115

94-
// Gets parameters from command elements.
95-
ceAsts = cmdAst.CommandElements.Where<CommandElementAst>(foundParamASTs);
116+
// We want to check cmdlets from PackageManagement separately because they experience a deadlock
117+
// when cmdInfo.Parameters or cmdInfo.ParameterSets is accessed.
118+
// See https://github.com/PowerShell/PSScriptAnalyzer/issues/1297
119+
if (s_pkgMgmtMandatoryParameters.TryGetValue(cmdInfo.Name, out IReadOnlyList<string> pkgMgmtCmdletMandatoryParams))
120+
{
121+
// If the command has no parameter sets with mandatory parameters, we are done
122+
if (pkgMgmtCmdletMandatoryParams.Count == 0)
123+
{
124+
return true;
125+
}
126+
127+
// We make the following simplifications here that all apply to the PackageManagement cmdlets:
128+
// - Only one mandatory parameter per parameter set
129+
// - Any part of the parameter prefix is valid
130+
// - There are no parameter sets without mandatory parameters
131+
IEnumerable<CommandParameterAst> parameterAsts = cmdAst.CommandElements.OfType<CommandParameterAst>();
132+
foreach (string mandatoryParameter in pkgMgmtCmdletMandatoryParams)
133+
{
134+
foreach (CommandParameterAst parameterAst in parameterAsts)
135+
{
136+
if (mandatoryParameter.StartsWith(parameterAst.ParameterName))
137+
{
138+
return true;
139+
}
140+
}
141+
}
142+
143+
return false;
144+
}
96145

97146
// Gets mandatory parameters from cmdlet.
98147
// If cannot find any mandatory parameter, it's not necessary to do a further check for current cmdlet.
148+
var mandatoryParameters = new List<ParameterMetadata>();
99149
try
100150
{
101151
int noOfParamSets = cmdInfo.ParameterSets.Count;
@@ -119,7 +169,7 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
119169

120170
if (count >= noOfParamSets)
121171
{
122-
mandParams.Add(pm);
172+
mandatoryParameters.Add(pm);
123173
}
124174
}
125175
}
@@ -129,28 +179,25 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
129179
return true;
130180
}
131181

132-
if (mandParams.Count == 0 || (Helper.Instance.IsKnownCmdletFunctionOrExternalScript(cmdAst) && Helper.Instance.PositionalParameterUsed(cmdAst)))
182+
if (mandatoryParameters.Count == 0)
133183
{
134-
returnValue = true;
184+
return true;
135185
}
136-
else
186+
187+
// Compares parameter list and mandatory parameter list.
188+
foreach (CommandElementAst commandElementAst in cmdAst.CommandElements.OfType<CommandParameterAst>())
137189
{
138-
// Compares parameter list and mandatory parameter list.
139-
foreach (CommandElementAst ceAst in ceAsts)
190+
CommandParameterAst cpAst = (CommandParameterAst)commandElementAst;
191+
if (mandatoryParameters.Count<ParameterMetadata>(item =>
192+
item.Name.Equals(cpAst.ParameterName, StringComparison.OrdinalIgnoreCase)) > 0)
140193
{
141-
CommandParameterAst cpAst = (CommandParameterAst)ceAst;
142-
if (mandParams.Count<ParameterMetadata>(item =>
143-
item.Name.Equals(cpAst.ParameterName, StringComparison.OrdinalIgnoreCase)) > 0)
144-
{
145-
returnValue = true;
146-
break;
147-
}
194+
return true;
148195
}
149196
}
150197

151198
#endregion
152199

153-
return returnValue;
200+
return false;
154201
}
155202

156203
/// <summary>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
Describe "Complex file analysis" {
5+
It "Analyzes file successfully" {
6+
Invoke-ScriptAnalyzer -Path "$PSScriptRoot/DeadlockTestAssets/complex.psm1"
7+
}
8+
}

0 commit comments

Comments
 (0)