Skip to content

Fix PSSA settings discovery + pick up changes to settings file #1206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 26, 2020
154 changes: 135 additions & 19 deletions src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -77,6 +79,10 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic)
"PSPossibleIncorrectUsageOfRedirectionOperator"
};

private static readonly StringComparison s_osPathStringComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;

private readonly ILoggerFactory _loggerFactory;

private readonly ILogger _logger;
Expand All @@ -91,7 +97,9 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic)

private readonly ConcurrentDictionary<ScriptFile, CorrectionTableEntry> _mostRecentCorrectionsByFile;

private Lazy<PssaCmdletAnalysisEngine> _analysisEngine;
private Lazy<PssaCmdletAnalysisEngine> _analysisEngineLazy;

private FileSystemWatcher _pssaSettingsFileWatcher;

private CancellationTokenSource _diagnosticsCancellationTokenSource;

Expand All @@ -115,13 +123,13 @@ public AnalysisService(
_workplaceService = workspaceService;
_analysisDelayMillis = 750;
_mostRecentCorrectionsByFile = new ConcurrentDictionary<ScriptFile, CorrectionTableEntry>();
_analysisEngine = new Lazy<PssaCmdletAnalysisEngine>(InstantiateAnalysisEngine);
_analysisEngineLazy = new Lazy<PssaCmdletAnalysisEngine>(InstantiateAnalysisEngine);
}

/// <summary>
/// The analysis engine to use for running script analysis.
/// </summary>
private PssaCmdletAnalysisEngine AnalysisEngine => _analysisEngine.Value;
private PssaCmdletAnalysisEngine AnalysisEngine => _analysisEngineLazy?.Value;

/// <summary>
/// Sets up a script analysis run, eventually returning the result.
Expand Down Expand Up @@ -254,23 +262,60 @@ public void ClearMarkers(ScriptFile file)
/// <param name="settings">The new language server settings.</param>
public void OnConfigurationUpdated(object sender, LanguageServerSettings settings)
{
ClearOpenFileMarkers();
_analysisEngine = new Lazy<PssaCmdletAnalysisEngine>(InstantiateAnalysisEngine);
InitializeAnalysisEngineToCurrentSettings();
}

private PssaCmdletAnalysisEngine InstantiateAnalysisEngine()
private void OnSettingsFileUpdated(object sender, FileSystemEventArgs args)
{
if (!(_configurationService.CurrentSettings.ScriptAnalysis.Enable ?? false))
InitializeAnalysisEngineToCurrentSettings();
}

private void InitializeAnalysisEngineToCurrentSettings()
{
// If script analysis has been disabled, just return null
if (_configurationService.CurrentSettings.ScriptAnalysis.Enable != true)
{
return null;
_pssaSettingsFileWatcher?.Dispose();
_pssaSettingsFileWatcher = null;

if (_analysisEngineLazy != null && _analysisEngineLazy.IsValueCreated)
{
_analysisEngineLazy.Value.Dispose();
}

_analysisEngineLazy = null;
return;
}

// We may be triggered after the lazy factory is set,
// but before it's been able to instantiate
if (_analysisEngineLazy == null)
{
_analysisEngineLazy = new Lazy<PssaCmdletAnalysisEngine>(InstantiateAnalysisEngine);
return;
}
else if (!_analysisEngineLazy.IsValueCreated)
{
return;
}

// Retrieve the current script analysis engine so we can recreate it after we've overridden it
PssaCmdletAnalysisEngine currentAnalysisEngine = AnalysisEngine;

// Clear the open file markers and set the new engine factory
ClearOpenFileMarkers();
_analysisEngineLazy = new Lazy<PssaCmdletAnalysisEngine>(() => RecreateAnalysisEngine(currentAnalysisEngine));
}

private PssaCmdletAnalysisEngine InstantiateAnalysisEngine()
{
var pssaCmdletEngineBuilder = new PssaCmdletAnalysisEngine.Builder(_loggerFactory);

// If there's a settings file use that
if (TryFindSettingsFile(out string settingsFilePath))
{
_logger.LogInformation($"Configuring PSScriptAnalyzer with rules at '{settingsFilePath}'");
SetSettingsFileWatcher(settingsFilePath);
pssaCmdletEngineBuilder.WithSettingsFile(settingsFilePath);
}
else
Expand All @@ -282,26 +327,90 @@ private PssaCmdletAnalysisEngine InstantiateAnalysisEngine()
return pssaCmdletEngineBuilder.Build();
}

private PssaCmdletAnalysisEngine RecreateAnalysisEngine(PssaCmdletAnalysisEngine oldAnalysisEngine)
{
if (TryFindSettingsFile(out string settingsFilePath))
{
_logger.LogInformation($"Recreating analysis engine with rules at '{settingsFilePath}'");
SetSettingsFileWatcher(settingsFilePath);
return oldAnalysisEngine.RecreateWithNewSettings(settingsFilePath);
}

_logger.LogInformation("PSScriptAnalyzer settings file not found. Falling back to default rules");
return oldAnalysisEngine.RecreateWithRules(s_defaultRules);
}

private void SetSettingsFileWatcher(string path)
{
string dirPath = Path.GetDirectoryName(path);
string fileName = Path.GetFileName(path);

if (_pssaSettingsFileWatcher != null)
{
if (string.Equals(dirPath, _pssaSettingsFileWatcher.Path, s_osPathStringComparison))
{
if (string.Equals(fileName, _pssaSettingsFileWatcher.Filter, s_osPathStringComparison))
{
// The current watcher is already watching the right file, so we are done
return;
}

// We just need to update the filter, which we can do without recreating the watcher
_pssaSettingsFileWatcher.Filter = fileName;
return;
}

// Otherwise we need to remove the old watcher
// and create a new one
DisposeCurrentSettingsFileWatcher();
}

_pssaSettingsFileWatcher = new FileSystemWatcher(dirPath)
{
Filter = fileName,
EnableRaisingEvents = true,
};
_pssaSettingsFileWatcher.Created += OnSettingsFileUpdated;
_pssaSettingsFileWatcher.Changed += OnSettingsFileUpdated;
_pssaSettingsFileWatcher.Deleted += OnSettingsFileUpdated;
}

private bool TryFindSettingsFile(out string settingsFilePath)
{
string configuredPath = _configurationService.CurrentSettings.ScriptAnalysis.SettingsPath;

if (!string.IsNullOrEmpty(configuredPath))
if (string.IsNullOrEmpty(configuredPath))
{
settingsFilePath = _workplaceService.ResolveWorkspacePath(configuredPath);
settingsFilePath = null;
return false;
}

if (settingsFilePath == null)
{
_logger.LogError($"Unable to find PSSA settings file at '{configuredPath}'. Loading default rules.");
}
settingsFilePath = _workplaceService.ResolveWorkspacePath(configuredPath);

return settingsFilePath != null;
if (settingsFilePath == null
|| !File.Exists(settingsFilePath))
{
_logger.LogWarning($"Unable to find PSSA settings file at '{configuredPath}'. Loading default rules.");
settingsFilePath = null;
return false;
}

// TODO: Could search for a default here
return true;
}

settingsFilePath = null;
return false;
private void DisposeCurrentSettingsFileWatcher()
{
if (_pssaSettingsFileWatcher == null)
{
return;
}

_pssaSettingsFileWatcher.Created -= OnSettingsFileUpdated;
_pssaSettingsFileWatcher.Changed -= OnSettingsFileUpdated;
_pssaSettingsFileWatcher.Deleted -= OnSettingsFileUpdated;

_pssaSettingsFileWatcher.Dispose();
_pssaSettingsFileWatcher = null;
}

private void ClearOpenFileMarkers()
Expand Down Expand Up @@ -442,8 +551,15 @@ protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_analysisEngine.IsValueCreated) { _analysisEngine.Value.Dispose(); }
if (_analysisEngineLazy != null
&& _analysisEngineLazy.IsValueCreated)
{
_analysisEngineLazy.Value.Dispose();
}

_diagnosticsCancellationTokenSource?.Dispose();

DisposeCurrentSettingsFileWatcher();
}

disposedValue = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,21 @@ public Task<ScriptFileMarker[]> AnalyzeScriptAsync(string scriptContent, Hashtab
return GetSemanticMarkersFromCommandAsync(command);
}

public PssaCmdletAnalysisEngine RecreateWithNewSettings(string settingsPath)
{
return new PssaCmdletAnalysisEngine(_logger, _analysisRunspacePool, _pssaModuleInfo, settingsPath);
}

public PssaCmdletAnalysisEngine RecreateWithNewSettings(Hashtable settingsHashtable)
{
return new PssaCmdletAnalysisEngine(_logger, _analysisRunspacePool, _pssaModuleInfo, settingsHashtable);
}

public PssaCmdletAnalysisEngine RecreateWithRules(string[] rules)
{
return new PssaCmdletAnalysisEngine(_logger, _analysisRunspacePool, _pssaModuleInfo, rules);
}

#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls

Expand Down