1
1
// Copyright (c) Microsoft Corporation.
2
2
// Licensed under the MIT License.
3
3
4
- using Microsoft . Extensions . Logging ;
5
- using Microsoft . PowerShell . EditorServices . Services . TextDocument ;
6
4
using System ;
7
5
using System . Collections ;
8
6
using System . Collections . Generic ;
9
7
using System . Collections . ObjectModel ;
10
8
using System . IO ;
11
9
using System . Linq ;
12
- using System . Management . Automation ;
13
- using System . Management . Automation . Runspaces ;
14
10
using System . Text ;
15
- using System . Threading ;
16
11
using System . Threading . Tasks ;
12
+ using Microsoft . Extensions . Logging ;
13
+ using Microsoft . PowerShell . EditorServices . Services . TextDocument ;
17
14
18
15
namespace Microsoft . PowerShell . EditorServices . Services . Analysis
19
16
{
17
+ using System . Management . Automation ;
18
+ using System . Management . Automation . Runspaces ;
19
+
20
20
/// <summary>
21
21
/// PowerShell script analysis engine that uses PSScriptAnalyzer
22
22
/// cmdlets run through a PowerShell API to drive analysis.
@@ -37,7 +37,7 @@ public class Builder
37
37
/// <summary>
38
38
/// Create a builder for PssaCmdletAnalysisEngine construction.
39
39
/// </summary>
40
- /// <param name="logger ">The logger to use.</param>
40
+ /// <param name="loggerFactory ">The logger to use.</param>
41
41
public Builder ( ILoggerFactory loggerFactory ) => _loggerFactory = loggerFactory ;
42
42
43
43
/// <summary>
@@ -51,17 +51,6 @@ public Builder WithSettingsFile(string settingsPath)
51
51
return this ;
52
52
}
53
53
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
-
65
54
/// <summary>
66
55
/// Uses a set of unconfigured rules for PSSA configuration.
67
56
/// </summary>
@@ -85,11 +74,11 @@ public PssaCmdletAnalysisEngine Build()
85
74
ILogger logger = _loggerFactory . CreateLogger < PssaCmdletAnalysisEngine > ( ) ;
86
75
try
87
76
{
88
- RunspacePool pssaRunspacePool = CreatePssaRunspacePool ( out PSModuleInfo pssaModuleInfo ) ;
77
+ RunspacePool pssaRunspacePool = CreatePssaRunspacePool ( ) ;
89
78
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 ) ;
93
82
94
83
cmdletAnalysisEngine . LogAvailablePssaFeatures ( ) ;
95
84
return cmdletAnalysisEngine ;
@@ -102,7 +91,10 @@ public PssaCmdletAnalysisEngine Build()
102
91
}
103
92
}
104
93
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" ) ) ;
106
98
107
99
/// <summary>
108
100
/// The indentation to add when the logger lists errors.
@@ -122,34 +114,28 @@ public PssaCmdletAnalysisEngine Build()
122
114
123
115
private readonly RunspacePool _analysisRunspacePool ;
124
116
125
- private readonly PSModuleInfo _pssaModuleInfo ;
126
-
127
117
private readonly object _settingsParameter ;
128
118
129
119
private readonly string [ ] _rulesToInclude ;
130
120
131
121
private PssaCmdletAnalysisEngine (
132
122
ILogger logger ,
133
123
RunspacePool analysisRunspacePool ,
134
- PSModuleInfo pssaModuleInfo ,
135
124
string [ ] rulesToInclude )
136
- : this ( logger , analysisRunspacePool , pssaModuleInfo ) => _rulesToInclude = rulesToInclude ;
125
+ : this ( logger , analysisRunspacePool ) => _rulesToInclude = rulesToInclude ;
137
126
138
127
private PssaCmdletAnalysisEngine (
139
128
ILogger logger ,
140
129
RunspacePool analysisRunspacePool ,
141
- PSModuleInfo pssaModuleInfo ,
142
130
object analysisSettingsParameter )
143
- : this ( logger , analysisRunspacePool , pssaModuleInfo ) => _settingsParameter = analysisSettingsParameter ;
131
+ : this ( logger , analysisRunspacePool ) => _settingsParameter = analysisSettingsParameter ;
144
132
145
133
private PssaCmdletAnalysisEngine (
146
134
ILogger logger ,
147
- RunspacePool analysisRunspacePool ,
148
- PSModuleInfo pssaModuleInfo )
135
+ RunspacePool analysisRunspacePool )
149
136
{
150
137
_logger = logger ;
151
138
_analysisRunspacePool = analysisRunspacePool ;
152
- _pssaModuleInfo = pssaModuleInfo ;
153
139
}
154
140
155
141
/// <summary>
@@ -158,14 +144,14 @@ private PssaCmdletAnalysisEngine(
158
144
/// <param name="scriptDefinition">The full text of a script.</param>
159
145
/// <param name="formatSettings">The formatter settings to use.</param>
160
146
/// <param name="rangeList">A possible range over which to run the formatter.</param>
161
- /// <returns></returns>
147
+ /// <returns>Formatted script as string </returns>
162
148
public async Task < string > FormatAsync ( string scriptDefinition , Hashtable formatSettings , int [ ] rangeList )
163
149
{
164
150
// We cannot use Range type therefore this workaround of using -1 default value.
165
151
// Invoke-Formatter throws a ParameterBinderValidationException if the ScriptDefinition is an empty string.
166
152
if ( string . IsNullOrEmpty ( scriptDefinition ) )
167
153
{
168
- _logger . LogDebug ( "Script Definition was: " + scriptDefinition == null ? "null" : "empty string" ) ;
154
+ _logger . LogDebug ( "Script Definition was: " + scriptDefinition is null ? "null" : "empty string" ) ;
169
155
return scriptDefinition ;
170
156
}
171
157
@@ -174,17 +160,17 @@ public async Task<string> FormatAsync(string scriptDefinition, Hashtable formatS
174
160
. AddParameter ( "ScriptDefinition" , scriptDefinition )
175
161
. AddParameter ( "Settings" , formatSettings ) ;
176
162
177
- if ( rangeList != null )
163
+ if ( rangeList is not null )
178
164
{
179
165
psCommand . AddParameter ( "Range" , rangeList ) ;
180
166
}
181
167
182
168
PowerShellResult result = await InvokePowerShellAsync ( psCommand ) . ConfigureAwait ( false ) ;
183
169
184
- if ( result == null )
170
+ if ( result is null )
185
171
{
186
172
_logger . LogError ( "Formatter returned null result" ) ;
187
- return scriptDefinition ;
173
+ return null ;
188
174
}
189
175
190
176
if ( result . HasErrors )
@@ -195,7 +181,7 @@ public async Task<string> FormatAsync(string scriptDefinition, Hashtable formatS
195
181
errorBuilder . Append ( err ) . Append ( s_indentJoin ) ;
196
182
}
197
183
_logger . LogWarning ( $ "Errors found while formatting file: { errorBuilder } ") ;
198
- return scriptDefinition ;
184
+ return null ;
199
185
}
200
186
201
187
foreach ( PSObject resultObj in result . Output )
@@ -206,8 +192,8 @@ public async Task<string> FormatAsync(string scriptDefinition, Hashtable formatS
206
192
}
207
193
}
208
194
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 ;
211
197
}
212
198
213
199
/// <summary>
@@ -239,7 +225,7 @@ public Task<ScriptFileMarker[]> AnalyzeScriptAsync(string scriptContent, Hashtab
239
225
. AddParameter ( "Severity" , s_scriptMarkerLevels ) ;
240
226
241
227
object settingsValue = settings ?? _settingsParameter ;
242
- if ( settingsValue != null )
228
+ if ( settingsValue is not null )
243
229
{
244
230
command . AddParameter ( "Settings" , settingsValue ) ;
245
231
}
@@ -251,11 +237,9 @@ public Task<ScriptFileMarker[]> AnalyzeScriptAsync(string scriptContent, Hashtab
251
237
return GetSemanticMarkersFromCommandAsync ( command ) ;
252
238
}
253
239
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 ) ;
257
241
258
- public PssaCmdletAnalysisEngine RecreateWithRules ( string [ ] rules ) => new ( _logger , _analysisRunspacePool , _pssaModuleInfo , rules ) ;
242
+ public PssaCmdletAnalysisEngine RecreateWithRules ( string [ ] rules ) => new ( _logger , _analysisRunspacePool , rules ) ;
259
243
260
244
#region IDisposable Support
261
245
private bool disposedValue ; // To detect redundant calls
@@ -298,19 +282,20 @@ private async Task<ScriptFileMarker[]> GetSemanticMarkersFromCommandAsync(PSComm
298
282
return scriptMarkers ;
299
283
}
300
284
285
+ // TODO: Deduplicate this logic and cleanup using lessons learned from pipeline rewrite.
301
286
private Task < PowerShellResult > InvokePowerShellAsync ( PSCommand command ) => Task . Run ( ( ) => InvokePowerShell ( command ) ) ;
302
287
303
288
private PowerShellResult InvokePowerShell ( PSCommand command )
304
289
{
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 ;
308
293
PowerShellResult result = null ;
309
294
try
310
295
{
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 ) ;
314
299
}
315
300
catch ( CommandNotFoundException ex )
316
301
{
@@ -330,20 +315,6 @@ private PowerShellResult InvokePowerShell(PSCommand command)
330
315
return result ;
331
316
}
332
317
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
-
347
318
/// <summary>
348
319
/// Log the features available from the PSScriptAnalyzer module that has been imported
349
320
/// for use with the AnalysisService.
@@ -356,32 +327,13 @@ private void LogAvailablePssaFeatures()
356
327
return ;
357
328
}
358
329
359
- if ( _pssaModuleInfo == null )
360
- {
361
- throw new FileNotFoundException ( "Unable to find loaded PSScriptAnalyzer module for logging" ) ;
362
- }
363
-
364
330
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:" ) ;
378
332
379
333
// Log available rules
380
- sb . AppendLine ( " Available Rules:" ) ;
381
334
foreach ( string ruleName in GetPSScriptAnalyzerRules ( ) )
382
335
{
383
- sb . Append ( " " ) ;
384
- sb . AppendLine ( ruleName ) ;
336
+ sb . Append ( " " ) . AppendLine ( ruleName ) ;
385
337
}
386
338
387
339
_logger . LogDebug ( sb . ToString ( ) ) ;
@@ -393,7 +345,7 @@ private void LogAvailablePssaFeatures()
393
345
private IEnumerable < string > GetPSScriptAnalyzerRules ( )
394
346
{
395
347
PowerShellResult getRuleResult = InvokePowerShell ( new PSCommand ( ) . AddCommand ( "Get-ScriptAnalyzerRule" ) ) ;
396
- if ( getRuleResult == null )
348
+ if ( getRuleResult is null )
397
349
{
398
350
_logger . LogWarning ( "Get-ScriptAnalyzerRule returned null result" ) ;
399
351
return Enumerable . Empty < string > ( ) ;
@@ -413,33 +365,9 @@ private IEnumerable<string> GetPSScriptAnalyzerRules()
413
365
/// This looks for the latest version of PSScriptAnalyzer on the path and loads that.
414
366
/// </summary>
415
367
/// <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 ( )
417
369
{
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 ) ;
443
371
444
372
// Now that we know where the PSScriptAnalyzer we want to use is, create a base
445
373
// session state with PSScriptAnalyzer loaded
@@ -448,18 +376,15 @@ private static RunspacePool CreatePssaRunspacePool(out PSModuleInfo pssaModuleIn
448
376
// only, which is a more minimal and therefore safer state.
449
377
InitialSessionState sessionState = InitialSessionState . CreateDefault2 ( ) ;
450
378
451
- sessionState . ImportPSModule ( new [ ] { pssaModuleInfo . ModuleBase } ) ;
379
+ sessionState . ImportPSModulesFromPath ( s_pssaModulePath ) ;
380
+ // pwsh.ImportModule(s_pssaModulePath);
381
+ // sessionState.ImportPSModule(new[] { pssaModuleInfo.ModuleBase });
452
382
453
383
RunspacePool runspacePool = RunspaceFactory . CreateRunspacePool ( sessionState ) ;
454
384
455
385
runspacePool . SetMaxRunspaces ( 1 ) ;
456
386
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 ( ) ;
463
388
464
389
return runspacePool ;
465
390
}
@@ -486,33 +411,5 @@ public PowerShellResult(
486
411
487
412
public bool HasErrors { get ; }
488
413
}
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
- }
517
414
}
518
415
}
0 commit comments