Skip to content

Commit f19f1b4

Browse files
Merge pull request #1764 from PowerShell/andschwa/pssa-fix
Fix formatting handlers and PSScriptAnalyzer loading
2 parents 2621e13 + 4786250 commit f19f1b4

File tree

2 files changed

+60
-151
lines changed

2 files changed

+60
-151
lines changed

src/PowerShellEditorServices/Services/Analysis/PssaCmdletAnalysisEngine.cs

+44-147
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using Microsoft.Extensions.Logging;
5-
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
64
using System;
75
using System.Collections;
86
using System.Collections.Generic;
97
using System.Collections.ObjectModel;
108
using System.IO;
119
using System.Linq;
12-
using System.Management.Automation;
13-
using System.Management.Automation.Runspaces;
1410
using System.Text;
15-
using System.Threading;
1611
using System.Threading.Tasks;
12+
using Microsoft.Extensions.Logging;
13+
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1714

1815
namespace 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

Comments
 (0)