Skip to content

Commit 1b77d3e

Browse files
authored
Merge pull request #340 from PowerShell/kapilmb/async-analysis-service
Make analysis service methods async
2 parents 1fce255 + 40b0e13 commit 1b77d3e

File tree

2 files changed

+125
-136
lines changed

2 files changed

+125
-136
lines changed

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ private async Task HandleScriptFileMarkersRequest(
239239
ScriptFileMarkerRequestParams requestParams,
240240
RequestContext<ScriptFileMarkerRequestResultParams> requestContext)
241241
{
242-
var markers = editorSession.AnalysisService.GetSemanticMarkers(
242+
var markers = await editorSession.AnalysisService.GetSemanticMarkersAsync(
243243
editorSession.Workspace.GetFile(requestParams.filePath),
244244
editorSession.AnalysisService.GetPSSASettingsHashtable(requestParams.settings));
245245
await requestContext.SendResult(new ScriptFileMarkerRequestResultParams {
@@ -1247,9 +1247,7 @@ private static async Task DelayThenInvokeDiagnostics(
12471247
{
12481248
Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath);
12491249

1250-
semanticMarkers =
1251-
editorSession.AnalysisService.GetSemanticMarkers(
1252-
scriptFile);
1250+
semanticMarkers = await editorSession.AnalysisService.GetSemanticMarkersAsync(scriptFile);
12531251

12541252
Logger.Write(LogLevel.Verbose, "Analysis complete.");
12551253
}

src/PowerShellEditorServices/Analysis/AnalysisService.cs

Lines changed: 123 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ public class AnalysisService : IDisposable
2525
{
2626
#region Private Fields
2727

28-
private Runspace analysisRunspace;
28+
private RunspacePool analysisRunspacePool;
2929
private PSModuleInfo scriptAnalyzerModuleInfo;
30-
private Object runspaceLock;
3130
private string[] activeRules;
3231
private string settingsPath;
3332

@@ -67,10 +66,7 @@ public string[] ActiveRules
6766

6867
set
6968
{
70-
lock (runspaceLock)
71-
{
72-
activeRules = value;
73-
}
69+
activeRules = value;
7470
}
7571
}
7672

@@ -86,10 +82,7 @@ public string SettingsPath
8682
}
8783
set
8884
{
89-
lock (runspaceLock)
90-
{
91-
settingsPath = value;
92-
}
85+
settingsPath = value;
9386
}
9487
}
9588

@@ -107,11 +100,21 @@ public AnalysisService(IConsoleHost consoleHost, string settingsPath = null)
107100
{
108101
try
109102
{
110-
this.runspaceLock = new Object();
111103
this.SettingsPath = settingsPath;
112-
this.analysisRunspace = RunspaceFactory.CreateRunspace(InitialSessionState.CreateDefault2());
113-
this.analysisRunspace.ThreadOptions = PSThreadOptions.ReuseThread;
114-
this.analysisRunspace.Open();
104+
var sessionState = InitialSessionState.CreateDefault2();
105+
106+
// import PSScriptAnalyzer in all runspaces
107+
sessionState.ImportPSModule(new string[] { "PSScriptAnalyzer" });
108+
109+
// runspacepool takes care of queuing commands for us so we do not
110+
// need to worry about executing concurrent commands
111+
this.analysisRunspacePool = RunspaceFactory.CreateRunspacePool(sessionState);
112+
113+
// having more than one runspace doesn't block code formatting if one
114+
// runspace is occupied for diagnostics
115+
this.analysisRunspacePool.SetMaxRunspaces(2);
116+
this.analysisRunspacePool.ThreadOptions = PSThreadOptions.ReuseThread;
117+
this.analysisRunspacePool.Open();
115118
ActiveRules = IncludedRules.ToArray();
116119
InitializePSScriptAnalyzer();
117120
}
@@ -134,9 +137,9 @@ public AnalysisService(IConsoleHost consoleHost, string settingsPath = null)
134137
/// </summary>
135138
/// <param name="file">The ScriptFile which will be analyzed for semantic markers.</param>
136139
/// <returns>An array of ScriptFileMarkers containing semantic analysis results.</returns>
137-
public ScriptFileMarker[] GetSemanticMarkers(ScriptFile file)
140+
public async Task<ScriptFileMarker[]> GetSemanticMarkersAsync(ScriptFile file)
138141
{
139-
return GetSemanticMarkers(file, activeRules, settingsPath);
142+
return await GetSemanticMarkersAsync(file, activeRules, settingsPath);
140143
}
141144

142145
/// <summary>
@@ -145,9 +148,9 @@ public ScriptFileMarker[] GetSemanticMarkers(ScriptFile file)
145148
/// <param name="file">The ScriptFile to be analyzed.</param>
146149
/// <param name="settings">ScriptAnalyzer settings</param>
147150
/// <returns></returns>
148-
public ScriptFileMarker[] GetSemanticMarkers(ScriptFile file, Hashtable settings)
151+
public async Task<ScriptFileMarker[]> GetSemanticMarkersAsync(ScriptFile file, Hashtable settings)
149152
{
150-
return GetSemanticMarkers<Hashtable>(file, null, settings);
153+
return await GetSemanticMarkersAsync<Hashtable>(file, null, settings);
151154
}
152155

153156
/// <summary>
@@ -158,17 +161,10 @@ public IEnumerable<string> GetPSScriptAnalyzerRules()
158161
List<string> ruleNames = new List<string>();
159162
if (scriptAnalyzerModuleInfo != null)
160163
{
161-
lock (runspaceLock)
164+
var ruleObjects = InvokePowerShell("Get-ScriptAnalyzerRule", new Dictionary<string, object>());
165+
foreach (var rule in ruleObjects)
162166
{
163-
using (var ps = System.Management.Automation.PowerShell.Create())
164-
{
165-
ps.Runspace = this.analysisRunspace;
166-
var ruleObjects = ps.AddCommand("Get-ScriptAnalyzerRule").Invoke();
167-
foreach (var rule in ruleObjects)
168-
{
169-
ruleNames.Add((string)rule.Members["RuleName"].Value);
170-
}
171-
}
167+
ruleNames.Add((string)rule.Members["RuleName"].Value);
172168
}
173169
}
174170

@@ -201,19 +197,19 @@ public Hashtable GetPSSASettingsHashtable(IDictionary<string, Hashtable> ruleSet
201197
/// </summary>
202198
public void Dispose()
203199
{
204-
if (this.analysisRunspace != null)
200+
if (this.analysisRunspacePool != null)
205201
{
206-
this.analysisRunspace.Close();
207-
this.analysisRunspace.Dispose();
208-
this.analysisRunspace = null;
202+
this.analysisRunspacePool.Close();
203+
this.analysisRunspacePool.Dispose();
204+
this.analysisRunspacePool = null;
209205
}
210206
}
211207

212208
#endregion // public methods
213209

214210
#region Private Methods
215211

216-
private ScriptFileMarker[] GetSemanticMarkers<TSettings>(
212+
private async Task<ScriptFileMarker[]> GetSemanticMarkersAsync<TSettings>(
217213
ScriptFile file,
218214
string[] rules,
219215
TSettings settings) where TSettings : class
@@ -223,23 +219,8 @@ private ScriptFileMarker[] GetSemanticMarkers<TSettings>(
223219
&& (typeof(TSettings) == typeof(string) || typeof(TSettings) == typeof(Hashtable))
224220
&& (rules != null || settings != null))
225221
{
226-
// TODO: This is a temporary fix until we can change how
227-
// ScriptAnalyzer invokes their async tasks.
228-
// TODO: Make this async
229-
Task<ScriptFileMarker[]> analysisTask =
230-
Task.Factory.StartNew<ScriptFileMarker[]>(
231-
() =>
232-
{
233-
return
234-
GetDiagnosticRecords(file, rules, settings)
235-
.Select(ScriptFileMarker.FromDiagnosticRecord)
236-
.ToArray();
237-
},
238-
CancellationToken.None,
239-
TaskCreationOptions.None,
240-
TaskScheduler.Default);
241-
analysisTask.Wait();
242-
return analysisTask.Result;
222+
var scriptFileMarkers = await GetDiagnosticRecordsAsync(file, rules, settings);
223+
return scriptFileMarkers.Select(ScriptFileMarker.FromDiagnosticRecord).ToArray();
243224
}
244225
else
245226
{
@@ -250,64 +231,53 @@ private ScriptFileMarker[] GetSemanticMarkers<TSettings>(
250231

251232
private void FindPSScriptAnalyzer()
252233
{
253-
lock (runspaceLock)
254-
{
255-
using (var ps = System.Management.Automation.PowerShell.Create())
234+
var modules = InvokePowerShell(
235+
"Get-Module",
236+
new Dictionary<string, object>
256237
{
257-
ps.Runspace = this.analysisRunspace;
258-
259-
var modules = ps.AddCommand("Get-Module")
260-
.AddParameter("List")
261-
.AddParameter("Name", "PSScriptAnalyzer")
262-
.Invoke();
263-
264-
var psModule = modules == null ? null : modules.FirstOrDefault();
265-
if (psModule != null)
266-
{
267-
scriptAnalyzerModuleInfo = psModule.ImmediateBaseObject as PSModuleInfo;
268-
Logger.Write(
269-
LogLevel.Normal,
270-
string.Format(
271-
"PSScriptAnalyzer found at {0}",
272-
scriptAnalyzerModuleInfo.Path));
273-
}
274-
else
275-
{
276-
Logger.Write(
277-
LogLevel.Normal,
278-
"PSScriptAnalyzer module was not found.");
279-
}
280-
}
238+
{ "ListAvailable", true },
239+
{ "Name", "PSScriptAnalyzer" }
240+
});
241+
var psModule = modules.Count() == 0 ? null : modules.FirstOrDefault();
242+
if (psModule != null)
243+
{
244+
scriptAnalyzerModuleInfo = psModule.ImmediateBaseObject as PSModuleInfo;
245+
Logger.Write(
246+
LogLevel.Normal,
247+
string.Format(
248+
"PSScriptAnalyzer found at {0}",
249+
scriptAnalyzerModuleInfo.Path));
250+
}
251+
else
252+
{
253+
Logger.Write(
254+
LogLevel.Normal,
255+
"PSScriptAnalyzer module was not found.");
281256
}
282257
}
283258

284259
private void ImportPSScriptAnalyzer()
285260
{
286261
if (scriptAnalyzerModuleInfo != null)
287262
{
288-
lock (runspaceLock)
289-
{
290-
using (var ps = System.Management.Automation.PowerShell.Create())
263+
var module = InvokePowerShell(
264+
"Import-Module",
265+
new Dictionary<string, object>
291266
{
292-
ps.Runspace = this.analysisRunspace;
293-
294-
var module = ps.AddCommand("Import-Module")
295-
.AddParameter("ModuleInfo", scriptAnalyzerModuleInfo)
296-
.AddParameter("PassThru")
297-
.Invoke();
298-
299-
if (module == null)
300-
{
301-
this.scriptAnalyzerModuleInfo = null;
302-
Logger.Write(LogLevel.Warning,
303-
String.Format("Cannot Import PSScriptAnalyzer: {0}"));
304-
}
305-
else
306-
{
307-
Logger.Write(LogLevel.Normal,
308-
String.Format("Successfully imported PSScriptAnalyzer"));
309-
}
310-
}
267+
{ "ModuleInfo", scriptAnalyzerModuleInfo },
268+
{ "PassThru", true },
269+
});
270+
271+
if (module.Count() == 0)
272+
{
273+
this.scriptAnalyzerModuleInfo = null;
274+
Logger.Write(LogLevel.Warning,
275+
String.Format("Cannot Import PSScriptAnalyzer: {0}"));
276+
}
277+
else
278+
{
279+
Logger.Write(LogLevel.Normal,
280+
String.Format("Successfully imported PSScriptAnalyzer"));
311281
}
312282
}
313283
}
@@ -331,54 +301,47 @@ private void EnumeratePSScriptAnalyzerRules()
331301
private void InitializePSScriptAnalyzer()
332302
{
333303
FindPSScriptAnalyzer();
304+
305+
// this import is redundant if we are importing the
306+
// module while creating the runspace, but it helps
307+
// us log the import related messages.
334308
ImportPSScriptAnalyzer();
335-
EnumeratePSScriptAnalyzerRules();
336-
}
337309

338-
private IEnumerable<PSObject> GetDiagnosticRecords(ScriptFile file)
339-
{
340-
return GetDiagnosticRecords(file, this.activeRules, this.settingsPath);
310+
EnumeratePSScriptAnalyzerRules();
341311
}
342312

343-
// TSettings can either be of type Hashtable or string
344-
// as scriptanalyzer settings parameter takes either a hashtable or string
345-
private IEnumerable<PSObject> GetDiagnosticRecords<TSettings>(
313+
private async Task<IEnumerable<PSObject>> GetDiagnosticRecordsAsync<TSettings>(
346314
ScriptFile file,
347315
string[] rules,
348-
TSettings settings) where TSettings: class
316+
TSettings settings) where TSettings : class
349317
{
350318
IEnumerable<PSObject> diagnosticRecords = Enumerable.Empty<PSObject>();
351319

352320
if (this.scriptAnalyzerModuleInfo != null
353321
&& (typeof(TSettings) == typeof(string)
354322
|| typeof(TSettings) == typeof(Hashtable)))
355323
{
356-
lock (runspaceLock)
324+
//Use a settings file if one is provided, otherwise use the default rule list.
325+
string settingParameter;
326+
object settingArgument;
327+
if (settings != null)
357328
{
358-
using (var powerShell = System.Management.Automation.PowerShell.Create())
359-
{
360-
powerShell.Runspace = this.analysisRunspace;
361-
Logger.Write(
362-
LogLevel.Verbose,
363-
String.Format("Running PSScriptAnalyzer against {0}", file.FilePath));
364-
365-
powerShell
366-
.AddCommand("Invoke-ScriptAnalyzer")
367-
.AddParameter("ScriptDefinition", file.Contents);
368-
369-
// Use a settings file if one is provided, otherwise use the default rule list.
370-
if (settings != null)
371-
{
372-
powerShell.AddParameter("Settings", settings);
373-
}
374-
else
375-
{
376-
powerShell.AddParameter("IncludeRule", rules);
377-
}
378-
379-
diagnosticRecords = powerShell.Invoke();
380-
}
329+
settingParameter = "Settings";
330+
settingArgument = settings;
381331
}
332+
else
333+
{
334+
settingParameter = "IncludeRule";
335+
settingArgument = rules;
336+
}
337+
338+
diagnosticRecords = await InvokePowerShellAsync(
339+
"Invoke-ScriptAnalyzer",
340+
new Dictionary<string, object>
341+
{
342+
{ "ScriptDefinition", file.Contents },
343+
{ settingParameter, settingArgument }
344+
});
382345
}
383346

384347
Logger.Write(
@@ -388,6 +351,34 @@ private IEnumerable<PSObject> GetDiagnosticRecords<TSettings>(
388351
return diagnosticRecords;
389352
}
390353

354+
private IEnumerable<PSObject> InvokePowerShell(string command, IDictionary<string, object> paramArgMap)
355+
{
356+
var task = InvokePowerShellAsync(command, paramArgMap);
357+
task.Wait();
358+
return task.Result;
359+
}
360+
361+
private async Task<IEnumerable<PSObject>> InvokePowerShellAsync(string command, IDictionary<string, object> paramArgMap)
362+
{
363+
using (var powerShell = System.Management.Automation.PowerShell.Create())
364+
{
365+
powerShell.RunspacePool = this.analysisRunspacePool;
366+
powerShell.AddCommand(command);
367+
foreach (var kvp in paramArgMap)
368+
{
369+
powerShell.AddParameter(kvp.Key, kvp.Value);
370+
}
371+
372+
var result = await Task.Factory.FromAsync(powerShell.BeginInvoke(), powerShell.EndInvoke);
373+
if (result == null)
374+
{
375+
return Enumerable.Empty<PSObject>();
376+
}
377+
378+
return result;
379+
}
380+
}
381+
391382
#endregion //private methods
392383
}
393384
}

0 commit comments

Comments
 (0)