33
44using System ;
55using System . Collections . Concurrent ;
6+ using System . Collections . Generic ;
67using System . Management . Automation ;
78using System . Linq ;
89using System . Management . Automation . Runspaces ;
@@ -14,6 +15,13 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
1415 /// </summary>
1516 internal class CommandInfoCache : IDisposable
1617 {
18+ /// <summary>
19+ /// Number of times a command lookup is attempted before giving up.
20+ /// Command lookups can fail transiently because the PowerShell engine is not thread safe,
21+ /// see https://github.com/PowerShell/PowerShell/issues/4003
22+ /// </summary>
23+ private const int MaxLookupAttempts = 3 ;
24+
1725 private readonly ConcurrentDictionary < CommandLookupKey , Lazy < CommandInfo > > _commandInfoCache ;
1826 private readonly RunspacePool _runspacePool ;
1927 private bool disposed = false ;
@@ -70,7 +78,21 @@ public CommandInfo GetCommandInfo(string commandName, CommandTypes? commandTypes
7078 return GetCommandInfoInternal ( commandName , commandTypes ) ;
7179 }
7280 // Atomically either use PowerShell to query a command info object, or fetch it from the cache
73- return _commandInfoCache . GetOrAdd ( key , new Lazy < CommandInfo > ( ( ) => GetCommandInfoInternal ( commandName , commandTypes ) ) ) . Value ;
81+ var lazyCommandInfo = _commandInfoCache . GetOrAdd ( key , new Lazy < CommandInfo > ( ( ) => GetCommandInfoInternal ( commandName , commandTypes ) ) ) ;
82+ try
83+ {
84+ return lazyCommandInfo . Value ;
85+ }
86+ catch
87+ {
88+ // Lazy<T> caches exceptions forever, which would make every subsequent lookup of this
89+ // command fail for the lifetime of the process. Evict the entry so that the next lookup
90+ // can try again. Only remove the faulted instance so that a replacement that another
91+ // thread may already have added is left alone.
92+ ( ( ICollection < KeyValuePair < CommandLookupKey , Lazy < CommandInfo > > > ) _commandInfoCache )
93+ . Remove ( new KeyValuePair < CommandLookupKey , Lazy < CommandInfo > > ( key , lazyCommandInfo ) ) ;
94+ throw ;
95+ }
7496 }
7597
7698
@@ -99,26 +121,46 @@ private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? command
99121 // For more details see https://github.com/PowerShell/PowerShell/issues/9308
100122 actualCmdName = WildcardPattern . Escape ( actualCmdName ) ;
101123
102- using ( var ps = System . Management . Automation . PowerShell . Create ( ) )
124+ for ( int attempt = 1 ; ; attempt ++ )
103125 {
104- ps . RunspacePool = _runspacePool ;
105-
106- ps . AddCommand ( "Get-Command" )
107- . AddParameter ( "Name" , actualCmdName )
108- . AddParameter ( "ErrorAction" , "SilentlyContinue" ) ;
109-
110- if ( commandType != null )
111- {
112- ps . AddParameter ( "CommandType" , commandType ) ;
113- }
114-
115- if ( ! string . IsNullOrEmpty ( moduleName ) )
126+ using ( var ps = System . Management . Automation . PowerShell . Create ( ) )
116127 {
117- ps . AddParameter ( "Module" , moduleName ) ;
128+ ps . RunspacePool = _runspacePool ;
129+
130+ ps . AddCommand ( "Get-Command" )
131+ . AddParameter ( "Name" , actualCmdName )
132+ . AddParameter ( "ErrorAction" , "SilentlyContinue" ) ;
133+
134+ if ( commandType != null )
135+ {
136+ ps . AddParameter ( "CommandType" , commandType ) ;
137+ }
138+
139+ if ( ! string . IsNullOrEmpty ( moduleName ) )
140+ {
141+ ps . AddParameter ( "Module" , moduleName ) ;
142+ }
143+
144+ try
145+ {
146+ return ps . Invoke < CommandInfo > ( )
147+ . FirstOrDefault ( ) ;
148+ }
149+ // 'Get-Command' is invoked with 'SilentlyContinue', so a CommandNotFoundException can only
150+ // mean that the engine failed to resolve 'Get-Command' itself in the pooled runspace.
151+ // That happens intermittently because the PowerShell engine is not thread safe, see
152+ // https://github.com/PowerShell/PowerShell/issues/4003 and
153+ // https://github.com/PowerShell/PSScriptAnalyzer/issues/2205
154+ // Retrying usually succeeds, but rather than failing the whole analysis when it does not,
155+ // treat the command as unresolvable.
156+ catch ( CommandNotFoundException )
157+ {
158+ if ( attempt >= MaxLookupAttempts )
159+ {
160+ return null ;
161+ }
162+ }
118163 }
119-
120- return ps . Invoke < CommandInfo > ( )
121- . FirstOrDefault ( ) ;
122164 }
123165 }
124166
0 commit comments