11// Copyright (c) Microsoft Corporation.
22// Licensed under the MIT License.
33
4- using Microsoft . Extensions . Logging ;
5- using Microsoft . PowerShell . EditorServices . Services . TextDocument ;
64using System ;
75using System . Collections ;
86using System . Collections . Generic ;
97using System . Collections . ObjectModel ;
108using System . IO ;
119using System . Linq ;
12- using System . Management . Automation ;
13- using System . Management . Automation . Runspaces ;
1410using System . Text ;
15- using System . Threading ;
1611using System . Threading . Tasks ;
12+ using Microsoft . Extensions . Logging ;
13+ using Microsoft . PowerShell . EditorServices . Services . TextDocument ;
1714
1815namespace Microsoft . PowerShell . EditorServices . Services . Analysis
1916{
17+ using System . Management . Automation ;
18+ using System . Management . Automation . Runspaces ;
19+
2020 /// <summary>
2121 /// PowerShell script analysis engine that uses PSScriptAnalyzer
2222 /// cmdlets run through a PowerShell API to drive analysis.
@@ -37,7 +37,7 @@ public class Builder
3737 /// <summary>
3838 /// Create a builder for PssaCmdletAnalysisEngine construction.
3939 /// </summary>
40- /// <param name="logger ">The logger to use.</param>
40+ /// <param name="loggerFactory ">The logger to use.</param>
4141 public Builder ( ILoggerFactory loggerFactory ) => _loggerFactory = loggerFactory ;
4242
4343 /// <summary>
@@ -51,17 +51,6 @@ public Builder WithSettingsFile(string settingsPath)
5151 return this ;
5252 }
5353
54- /// <summary>
55- /// Uses a settings hashtable for PSSA rule configuration.
56- /// </summary>
57- /// <param name="settings">The settings hashtable to pass to PSSA.</param>
58- /// <returns>The builder for chaining.</returns>
59- public Builder WithSettings ( Hashtable settings )
60- {
61- _settingsParameter = settings ;
62- return this ;
63- }
64-
6554 /// <summary>
6655 /// Uses a set of unconfigured rules for PSSA configuration.
6756 /// </summary>
@@ -85,11 +74,11 @@ public PssaCmdletAnalysisEngine Build()
8574 ILogger logger = _loggerFactory . CreateLogger < PssaCmdletAnalysisEngine > ( ) ;
8675 try
8776 {
88- RunspacePool pssaRunspacePool = CreatePssaRunspacePool ( out PSModuleInfo pssaModuleInfo ) ;
77+ RunspacePool pssaRunspacePool = CreatePssaRunspacePool ( ) ;
8978
90- PssaCmdletAnalysisEngine cmdletAnalysisEngine = _settingsParameter != null
91- ? new PssaCmdletAnalysisEngine ( logger , pssaRunspacePool , pssaModuleInfo , _settingsParameter )
92- : new PssaCmdletAnalysisEngine ( logger , pssaRunspacePool , pssaModuleInfo , _rules ) ;
79+ PssaCmdletAnalysisEngine cmdletAnalysisEngine = _settingsParameter is not null
80+ ? new PssaCmdletAnalysisEngine ( logger , pssaRunspacePool , _settingsParameter )
81+ : new PssaCmdletAnalysisEngine ( logger , pssaRunspacePool , _rules ) ;
9382
9483 cmdletAnalysisEngine . LogAvailablePssaFeatures ( ) ;
9584 return cmdletAnalysisEngine ;
@@ -102,7 +91,10 @@ public PssaCmdletAnalysisEngine Build()
10291 }
10392 }
10493
105- private const string PSSA_MODULE_NAME = "PSScriptAnalyzer" ;
94+ // This is a default that can be overriden at runtime by the user or tests.
95+ // TODO: Deduplicate this logic with PsesInternalHost.
96+ private static readonly string s_pssaModulePath = Path . GetFullPath ( Path . Combine (
97+ Path . GetDirectoryName ( typeof ( PssaCmdletAnalysisEngine ) . Assembly . Location ) , ".." , ".." , ".." , "PSScriptAnalyzer" ) ) ;
10698
10799 /// <summary>
108100 /// The indentation to add when the logger lists errors.
@@ -122,34 +114,28 @@ public PssaCmdletAnalysisEngine Build()
122114
123115 private readonly RunspacePool _analysisRunspacePool ;
124116
125- private readonly PSModuleInfo _pssaModuleInfo ;
126-
127117 private readonly object _settingsParameter ;
128118
129119 private readonly string [ ] _rulesToInclude ;
130120
131121 private PssaCmdletAnalysisEngine (
132122 ILogger logger ,
133123 RunspacePool analysisRunspacePool ,
134- PSModuleInfo pssaModuleInfo ,
135124 string [ ] rulesToInclude )
136- : this ( logger , analysisRunspacePool , pssaModuleInfo ) => _rulesToInclude = rulesToInclude ;
125+ : this ( logger , analysisRunspacePool ) => _rulesToInclude = rulesToInclude ;
137126
138127 private PssaCmdletAnalysisEngine (
139128 ILogger logger ,
140129 RunspacePool analysisRunspacePool ,
141- PSModuleInfo pssaModuleInfo ,
142130 object analysisSettingsParameter )
143- : this ( logger , analysisRunspacePool , pssaModuleInfo ) => _settingsParameter = analysisSettingsParameter ;
131+ : this ( logger , analysisRunspacePool ) => _settingsParameter = analysisSettingsParameter ;
144132
145133 private PssaCmdletAnalysisEngine (
146134 ILogger logger ,
147- RunspacePool analysisRunspacePool ,
148- PSModuleInfo pssaModuleInfo )
135+ RunspacePool analysisRunspacePool )
149136 {
150137 _logger = logger ;
151138 _analysisRunspacePool = analysisRunspacePool ;
152- _pssaModuleInfo = pssaModuleInfo ;
153139 }
154140
155141 /// <summary>
@@ -158,14 +144,14 @@ private PssaCmdletAnalysisEngine(
158144 /// <param name="scriptDefinition">The full text of a script.</param>
159145 /// <param name="formatSettings">The formatter settings to use.</param>
160146 /// <param name="rangeList">A possible range over which to run the formatter.</param>
161- /// <returns></returns>
147+ /// <returns>Formatted script as string </returns>
162148 public async Task < string > FormatAsync ( string scriptDefinition , Hashtable formatSettings , int [ ] rangeList )
163149 {
164150 // We cannot use Range type therefore this workaround of using -1 default value.
165151 // Invoke-Formatter throws a ParameterBinderValidationException if the ScriptDefinition is an empty string.
166152 if ( string . IsNullOrEmpty ( scriptDefinition ) )
167153 {
168- _logger . LogDebug ( "Script Definition was: " + scriptDefinition == null ? "null" : "empty string" ) ;
154+ _logger . LogDebug ( "Script Definition was: " + scriptDefinition is null ? "null" : "empty string" ) ;
169155 return scriptDefinition ;
170156 }
171157
@@ -174,17 +160,17 @@ public async Task<string> FormatAsync(string scriptDefinition, Hashtable formatS
174160 . AddParameter ( "ScriptDefinition" , scriptDefinition )
175161 . AddParameter ( "Settings" , formatSettings ) ;
176162
177- if ( rangeList != null )
163+ if ( rangeList is not null )
178164 {
179165 psCommand . AddParameter ( "Range" , rangeList ) ;
180166 }
181167
182168 PowerShellResult result = await InvokePowerShellAsync ( psCommand ) . ConfigureAwait ( false ) ;
183169
184- if ( result == null )
170+ if ( result is null )
185171 {
186172 _logger . LogError ( "Formatter returned null result" ) ;
187- return scriptDefinition ;
173+ return null ;
188174 }
189175
190176 if ( result . HasErrors )
@@ -195,7 +181,7 @@ public async Task<string> FormatAsync(string scriptDefinition, Hashtable formatS
195181 errorBuilder . Append ( err ) . Append ( s_indentJoin ) ;
196182 }
197183 _logger . LogWarning ( $ "Errors found while formatting file: { errorBuilder } ") ;
198- return scriptDefinition ;
184+ return null ;
199185 }
200186
201187 foreach ( PSObject resultObj in result . Output )
@@ -206,8 +192,8 @@ public async Task<string> FormatAsync(string scriptDefinition, Hashtable formatS
206192 }
207193 }
208194
209- _logger . LogError ( "Couldn't get result from output. Returning original script ." ) ;
210- return scriptDefinition ;
195+ _logger . LogError ( "Couldn't get result from output. Returning null ." ) ;
196+ return null ;
211197 }
212198
213199 /// <summary>
@@ -239,7 +225,7 @@ public Task<ScriptFileMarker[]> AnalyzeScriptAsync(string scriptContent, Hashtab
239225 . AddParameter ( "Severity" , s_scriptMarkerLevels ) ;
240226
241227 object settingsValue = settings ?? _settingsParameter ;
242- if ( settingsValue != null )
228+ if ( settingsValue is not null )
243229 {
244230 command . AddParameter ( "Settings" , settingsValue ) ;
245231 }
@@ -251,11 +237,9 @@ public Task<ScriptFileMarker[]> AnalyzeScriptAsync(string scriptContent, Hashtab
251237 return GetSemanticMarkersFromCommandAsync ( command ) ;
252238 }
253239
254- public PssaCmdletAnalysisEngine RecreateWithNewSettings ( string settingsPath ) => new ( _logger , _analysisRunspacePool , _pssaModuleInfo , settingsPath ) ;
255-
256- public PssaCmdletAnalysisEngine RecreateWithNewSettings ( Hashtable settingsHashtable ) => new ( _logger , _analysisRunspacePool , _pssaModuleInfo , settingsHashtable ) ;
240+ public PssaCmdletAnalysisEngine RecreateWithNewSettings ( string settingsPath ) => new ( _logger , _analysisRunspacePool , settingsPath ) ;
257241
258- public PssaCmdletAnalysisEngine RecreateWithRules ( string [ ] rules ) => new ( _logger , _analysisRunspacePool , _pssaModuleInfo , rules ) ;
242+ public PssaCmdletAnalysisEngine RecreateWithRules ( string [ ] rules ) => new ( _logger , _analysisRunspacePool , rules ) ;
259243
260244 #region IDisposable Support
261245 private bool disposedValue ; // To detect redundant calls
@@ -298,19 +282,20 @@ private async Task<ScriptFileMarker[]> GetSemanticMarkersFromCommandAsync(PSComm
298282 return scriptMarkers ;
299283 }
300284
285+ // TODO: Deduplicate this logic and cleanup using lessons learned from pipeline rewrite.
301286 private Task < PowerShellResult > InvokePowerShellAsync ( PSCommand command ) => Task . Run ( ( ) => InvokePowerShell ( command ) ) ;
302287
303288 private PowerShellResult InvokePowerShell ( PSCommand command )
304289 {
305- using System . Management . Automation . PowerShell powerShell = System . Management . Automation . PowerShell . Create ( RunspaceMode . NewRunspace ) ;
306- powerShell . RunspacePool = _analysisRunspacePool ;
307- powerShell . Commands = command ;
290+ using PowerShell pwsh = PowerShell . Create ( RunspaceMode . NewRunspace ) ;
291+ pwsh . RunspacePool = _analysisRunspacePool ;
292+ pwsh . Commands = command ;
308293 PowerShellResult result = null ;
309294 try
310295 {
311- Collection < PSObject > output = InvokePowerShellWithModulePathPreservation ( powerShell ) ;
312- PSDataCollection < ErrorRecord > errors = powerShell . Streams . Error ;
313- result = new PowerShellResult ( output , errors , powerShell . HadErrors ) ;
296+ Collection < PSObject > output = pwsh . Invoke ( ) ;
297+ PSDataCollection < ErrorRecord > errors = pwsh . Streams . Error ;
298+ result = new PowerShellResult ( output , errors , pwsh . HadErrors ) ;
314299 }
315300 catch ( CommandNotFoundException ex )
316301 {
@@ -330,20 +315,6 @@ private PowerShellResult InvokePowerShell(PSCommand command)
330315 return result ;
331316 }
332317
333- /// <summary>
334- /// Execute PSScriptAnalyzer cmdlets in PowerShell while preserving the PSModulePath.
335- /// Attempts to prevent PSModulePath mutation by runspace creation within the PSScriptAnalyzer module.
336- /// </summary>
337- /// <param name="powershell">The PowerShell instance to execute.</param>
338- /// <returns>The output of PowerShell execution.</returns>
339- private static Collection < PSObject > InvokePowerShellWithModulePathPreservation ( System . Management . Automation . PowerShell powershell )
340- {
341- using ( PSModulePathPreserver . Take ( ) )
342- {
343- return powershell . Invoke ( ) ;
344- }
345- }
346-
347318 /// <summary>
348319 /// Log the features available from the PSScriptAnalyzer module that has been imported
349320 /// for use with the AnalysisService.
@@ -356,32 +327,13 @@ private void LogAvailablePssaFeatures()
356327 return ;
357328 }
358329
359- if ( _pssaModuleInfo == null )
360- {
361- throw new FileNotFoundException ( "Unable to find loaded PSScriptAnalyzer module for logging" ) ;
362- }
363-
364330 StringBuilder sb = new ( ) ;
365- sb . AppendLine ( "PSScriptAnalyzer successfully imported:" ) ;
366-
367- // Log version
368- sb . Append ( " Version: " ) ;
369- sb . AppendLine ( _pssaModuleInfo . Version . ToString ( ) ) ;
370-
371- // Log exported cmdlets
372- sb . AppendLine ( " Exported Cmdlets:" ) ;
373- foreach ( string cmdletName in _pssaModuleInfo . ExportedCmdlets . Keys . OrderBy ( name => name ) )
374- {
375- sb . Append ( " " ) ;
376- sb . AppendLine ( cmdletName ) ;
377- }
331+ sb . AppendLine ( "PSScriptAnalyzer successfully imported:" ) . AppendLine ( " Available Rules:" ) ;
378332
379333 // Log available rules
380- sb . AppendLine ( " Available Rules:" ) ;
381334 foreach ( string ruleName in GetPSScriptAnalyzerRules ( ) )
382335 {
383- sb . Append ( " " ) ;
384- sb . AppendLine ( ruleName ) ;
336+ sb . Append ( " " ) . AppendLine ( ruleName ) ;
385337 }
386338
387339 _logger . LogDebug ( sb . ToString ( ) ) ;
@@ -393,7 +345,7 @@ private void LogAvailablePssaFeatures()
393345 private IEnumerable < string > GetPSScriptAnalyzerRules ( )
394346 {
395347 PowerShellResult getRuleResult = InvokePowerShell ( new PSCommand ( ) . AddCommand ( "Get-ScriptAnalyzerRule" ) ) ;
396- if ( getRuleResult == null )
348+ if ( getRuleResult is null )
397349 {
398350 _logger . LogWarning ( "Get-ScriptAnalyzerRule returned null result" ) ;
399351 return Enumerable . Empty < string > ( ) ;
@@ -413,33 +365,9 @@ private IEnumerable<string> GetPSScriptAnalyzerRules()
413365 /// This looks for the latest version of PSScriptAnalyzer on the path and loads that.
414366 /// </summary>
415367 /// <returns>A runspace pool with PSScriptAnalyzer loaded for running script analysis tasks.</returns>
416- private static RunspacePool CreatePssaRunspacePool ( out PSModuleInfo pssaModuleInfo )
368+ private static RunspacePool CreatePssaRunspacePool ( )
417369 {
418- using System . Management . Automation . PowerShell ps = System . Management . Automation . PowerShell . Create ( RunspaceMode . NewRunspace ) ;
419- // Run `Get-Module -ListAvailable -Name "PSScriptAnalyzer"`
420- ps . AddCommand ( "Get-Module" )
421- . AddParameter ( "ListAvailable" )
422- . AddParameter ( "Name" , PSSA_MODULE_NAME ) ;
423-
424- try
425- {
426- using ( PSModulePathPreserver . Take ( ) )
427- {
428- // Get the latest version of PSScriptAnalyzer we can find
429- pssaModuleInfo = ps . Invoke < PSModuleInfo > ( ) ?
430- . OrderByDescending ( moduleInfo => moduleInfo . Version )
431- . FirstOrDefault ( ) ;
432- }
433- }
434- catch ( Exception e )
435- {
436- throw new FileNotFoundException ( "Unable to find PSScriptAnalyzer module on the module path" , e ) ;
437- }
438-
439- if ( pssaModuleInfo == null )
440- {
441- throw new FileNotFoundException ( "Unable to find PSScriptAnalyzer module on the module path" ) ;
442- }
370+ using PowerShell pwsh = PowerShell . Create ( RunspaceMode . NewRunspace ) ;
443371
444372 // Now that we know where the PSScriptAnalyzer we want to use is, create a base
445373 // session state with PSScriptAnalyzer loaded
@@ -448,18 +376,15 @@ private static RunspacePool CreatePssaRunspacePool(out PSModuleInfo pssaModuleIn
448376 // only, which is a more minimal and therefore safer state.
449377 InitialSessionState sessionState = InitialSessionState . CreateDefault2 ( ) ;
450378
451- sessionState . ImportPSModule ( new [ ] { pssaModuleInfo . ModuleBase } ) ;
379+ sessionState . ImportPSModulesFromPath ( s_pssaModulePath ) ;
380+ // pwsh.ImportModule(s_pssaModulePath);
381+ // sessionState.ImportPSModule(new[] { pssaModuleInfo.ModuleBase });
452382
453383 RunspacePool runspacePool = RunspaceFactory . CreateRunspacePool ( sessionState ) ;
454384
455385 runspacePool . SetMaxRunspaces ( 1 ) ;
456386 runspacePool . ThreadOptions = PSThreadOptions . ReuseThread ;
457-
458- // Open the runspace pool here so we can deterministically handle the PSModulePath change issue
459- using ( PSModulePathPreserver . Take ( ) )
460- {
461- runspacePool . Open ( ) ;
462- }
387+ runspacePool . Open ( ) ;
463388
464389 return runspacePool ;
465390 }
@@ -486,33 +411,5 @@ public PowerShellResult(
486411
487412 public bool HasErrors { get ; }
488413 }
489-
490- /// <summary>
491- /// Struct to manage a call that may change the PSModulePath, so that it can be safely reset afterward.
492- /// </summary>
493- /// <remarks>
494- /// If the user manages to set the module path at the same time, using this struct may override that.
495- /// But this happening is less likely than the current issue where the PSModulePath is always reset.
496- /// </remarks>
497- private struct PSModulePathPreserver : IDisposable
498- {
499- private static readonly object s_psModulePathMutationLock = new ( ) ;
500-
501- public static PSModulePathPreserver Take ( )
502- {
503- Monitor . Enter ( s_psModulePathMutationLock ) ;
504- return new PSModulePathPreserver ( Environment . GetEnvironmentVariable ( "PSModulePath" ) ) ;
505- }
506-
507- private readonly string _psModulePath ;
508-
509- private PSModulePathPreserver ( string psModulePath ) => _psModulePath = psModulePath ;
510-
511- public void Dispose ( )
512- {
513- Environment . SetEnvironmentVariable ( "PSModulePath" , _psModulePath ) ;
514- Monitor . Exit ( s_psModulePathMutationLock ) ;
515- }
516- }
517414 }
518415}
0 commit comments