diff --git a/.gitignore b/.gitignore
index 99073998e..33a89bf3b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,7 @@ docs/_site/
docs/_repo/
docs/metadata/
tools/
+*.zip
# quickbuild.exe
/VersionGeneratingLogs/
@@ -67,4 +68,4 @@ module/PowerShellEditorServices/Commands/en-US/*-help.xml
module/PowerShellEditorServices/Third\ Party\ Notices.txt
# Visual Studio for Mac generated file
-*.userprefs
\ No newline at end of file
+*.userprefs
diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1
index eacc30251..1029d5623 100644
--- a/PowerShellEditorServices.build.ps1
+++ b/PowerShellEditorServices.build.ps1
@@ -5,7 +5,13 @@
param(
[ValidateSet("Debug", "Release")]
- [string]$Configuration = "Debug"
+ [string]$Configuration = "Debug",
+
+ [string]$PsesSubmodulePath = "$PSScriptRoot/module",
+
+ [string]$ModulesJsonPath = "$PSScriptRoot/modules.json",
+
+ [string]$DefaultModuleRepository = "PSGallery"
)
#Requires -Modules @{ModuleName="InvokeBuild";ModuleVersion="3.2.1"}
@@ -13,6 +19,7 @@ param(
$script:IsCIBuild = $env:APPVEYOR -ne $null
$script:IsUnix = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq "Core" -and !$IsWindows
$script:TargetFrameworksParam = "/p:TargetFrameworks=\`"$(if (!$script:IsUnix) { "net451;" })netstandard1.6\`""
+$script:SaveModuleSupportsAllowPrerelease = (Get-Command Save-Module).Parameters.ContainsKey("AllowPrerelease")
if ($PSVersionTable.PSEdition -ne "Core") {
Add-Type -Assembly System.IO.Compression.FileSystem
@@ -179,7 +186,7 @@ task TestProtocol -If { !$script:IsUnix} {
task TestHost -If { !$script:IsUnix} {
Set-Location .\test\PowerShellEditorServices.Test.Host\
exec { & $script:dotnetExe build -c $Configuration -f net452 }
- exec { & $script:dotnetExe xunit -configuration $Configuration -framework net452 -verbose -nobuild -x86 }
+ exec { & $script:dotnetExe xunit -configuration $Configuration -framework net452 -verbose -nobuild }
}
task CITest ?Test, {
@@ -220,6 +227,67 @@ task LayoutModule -After Build {
}
}
+task RestorePsesModules -After Build {
+ $submodulePath = (Resolve-Path $PsesSubmodulePath).Path + [IO.Path]::DirectorySeparatorChar
+ Write-Host "`nRestoring EditorServices modules..."
+
+ # Read in the modules.json file as a hashtable so it can be splatted
+ $moduleInfos = @{}
+
+ (Get-Content -Raw $ModulesJsonPath | ConvertFrom-Json).PSObject.Properties | ForEach-Object {
+ $name = $_.Name
+ $body = @{
+ Name = $name
+ MinimumVersion = $_.Value.MinimumVersion
+ MaximumVersion = $_.Value.MaximumVersion
+ Repository = if ($_.Value.Repository) { $_.Value.Repository } else { $DefaultModuleRepository }
+ Path = $submodulePath
+ }
+
+ if (-not $name)
+ {
+ throw "EditorServices module listed without name in '$ModulesJsonPath'"
+ }
+
+ if ($script:SaveModuleSupportsAllowPrerelease)
+ {
+ $body += @{ AllowPrerelease = $_.Value.AllowPrerelease }
+ }
+
+ $moduleInfos.Add($name, $body)
+ }
+
+ # Save each module in the modules.json file
+ foreach ($moduleName in $moduleInfos.Keys)
+ {
+ if (Test-Path -Path (Join-Path -Path $submodulePath -ChildPath $moduleName))
+ {
+ Write-Host "`tModule '${moduleName}' already detected. Skipping"
+ continue
+ }
+
+ $moduleInstallDetails = $moduleInfos[$moduleName]
+
+ $splatParameters = @{
+ Name = $moduleName
+ MinimumVersion = $moduleInstallDetails.MinimumVersion
+ MaximumVersion = $moduleInstallDetails.MaximumVersion
+ Repository = if ($moduleInstallDetails.Repository) { $moduleInstallDetails.Repository } else { $DefaultModuleRepository }
+ Path = $submodulePath
+ }
+
+ if ($script:SaveModuleSupportsAllowPrerelease)
+ {
+ $splatParameters += @{ AllowPrerelease = $moduleInstallDetails.AllowPrerelease }
+ }
+
+ Write-Host "`tInstalling module: ${moduleName}"
+
+ Save-Module @splatParameters
+ }
+ Write-Host "`n"
+}
+
task BuildCmdletHelp {
New-ExternalHelp -Path $PSScriptRoot\module\docs -OutputPath $PSScriptRoot\module\PowerShellEditorServices\Commands\en-US -Force
}
diff --git a/module/Start-EditorServices.ps1 b/module/PowerShellEditorServices/Start-EditorServices.ps1
similarity index 90%
rename from module/Start-EditorServices.ps1
rename to module/PowerShellEditorServices/Start-EditorServices.ps1
index a1f9152ab..f94691de1 100644
--- a/module/Start-EditorServices.ps1
+++ b/module/PowerShellEditorServices/Start-EditorServices.ps1
@@ -14,14 +14,9 @@
# canonical version of this script at the PowerShell Editor
# Services GitHub repository:
#
-# https://github.com/PowerShell/PowerShellEditorServices/blob/master/module/Start-EditorServices.ps1
+# https://github.com/PowerShell/PowerShellEditorServices/blob/master/module/PowerShellEditorServices/Start-EditorServices.ps1
param(
- [Parameter(Mandatory=$true)]
- [ValidateNotNullOrEmpty()]
- [string]
- $EditorServicesVersion,
-
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
@@ -97,6 +92,13 @@ function ExitWithError($errorString) {
exit 1;
}
+function WriteSessionFile($sessionInfo) {
+ $sessionInfoJson = ConvertTo-Json -InputObject $sessionInfo -Compress
+ Log "Writing session file with contents:"
+ Log $sessionInfoJson
+ $sessionInfoJson | Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop
+}
+
# Are we running in PowerShell 2 or earlier?
if ($PSVersionTable.PSVersion.Major -le 2) {
# No ConvertTo-Json on PSv2 and below, so write out the JSON manually
@@ -106,12 +108,6 @@ if ($PSVersionTable.PSVersion.Major -le 2) {
ExitWithError "Unsupported PowerShell version $($PSVersionTable.PSVersion), language features are disabled."
}
-function WriteSessionFile($sessionInfo) {
- $sessionInfoJson = ConvertTo-Json -InputObject $sessionInfo -Compress
- Log "Writing session file with contents:"
- Log $sessionInfoJson
- $sessionInfoJson | Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop
-}
if ($host.Runspace.LanguageMode -eq 'ConstrainedLanguage') {
WriteSessionFile @{
@@ -244,32 +240,11 @@ if ((Test-ModuleAvailable "PowerShellGet") -eq $false) {
# TODO: WRITE ERROR
}
-# Check if the expected version of the PowerShell Editor Services
-# module is installed
-$parsedVersion = New-Object System.Version @($EditorServicesVersion)
-if ((Test-ModuleAvailable "PowerShellEditorServices" $parsedVersion) -eq $false) {
- if ($ConfirmInstall -and $isPS5orLater) {
- # TODO: Check for error and return failure if necessary
- LogSection "Install PowerShellEditorServices"
- Install-Module "PowerShellEditorServices" -RequiredVersion $parsedVersion -Confirm
- }
- else {
- # Indicate to the client that the PowerShellEditorServices module
- # needs to be installed
- Write-Output "needs_install"
- }
-}
-
try {
LogSection "Start up PowerShellEditorServices"
Log "Importing PowerShellEditorServices"
- if ($isPS5orLater) {
- Import-Module PowerShellEditorServices -RequiredVersion $parsedVersion -ErrorAction Stop
- }
- else {
- Import-Module PowerShellEditorServices -Version $parsedVersion -ErrorAction Stop
- }
+ Import-Module PowerShellEditorServices -ErrorAction Stop
# Locate available port numbers for services
Log "Searching for available socket port for the language service"
diff --git a/modules.json b/modules.json
new file mode 100644
index 000000000..e4555e5de
--- /dev/null
+++ b/modules.json
@@ -0,0 +1,12 @@
+{
+ "PSScriptAnalyzer":{
+ "MinimumVersion":"1.6",
+ "MaximumVersion":"1.99",
+ "AllowPrerelease":false
+ },
+ "Plaster":{
+ "MinimumVersion":"1.0",
+ "MaximumVersion":"1.99",
+ "AllowPrerelease":false
+ }
+}
diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs
index 80c5746ca..38b3ff6bd 100644
--- a/src/PowerShellEditorServices/Analysis/AnalysisService.cs
+++ b/src/PowerShellEditorServices/Analysis/AnalysisService.cs
@@ -13,6 +13,7 @@
using System.Collections.Generic;
using System.Text;
using System.Collections;
+using System.IO;
namespace Microsoft.PowerShell.EditorServices
{
@@ -26,17 +27,12 @@ public class AnalysisService : IDisposable
private const int NumRunspaces = 1;
- private ILogger logger;
- private RunspacePool analysisRunspacePool;
- private PSModuleInfo scriptAnalyzerModuleInfo;
+ private const string PSSA_MODULE_NAME = "PSScriptAnalyzer";
- private bool hasScriptAnalyzerModule
- {
- get
- {
- return scriptAnalyzerModuleInfo != null;
- }
- }
+ private ILogger _logger;
+ private RunspacePool _analysisRunspacePool;
+
+ private bool _hasScriptAnalyzerModule;
private string[] activeRules;
private string settingsPath;
@@ -108,25 +104,30 @@ public string SettingsPath
/// An ILogger implementation used for writing log messages.
public AnalysisService(string settingsPath, ILogger logger)
{
- this.logger = logger;
+ this._logger = logger;
try
{
this.SettingsPath = settingsPath;
- scriptAnalyzerModuleInfo = FindPSScriptAnalyzerModule(logger);
- var sessionState = InitialSessionState.CreateDefault2();
- sessionState.ImportPSModulesFromPath(scriptAnalyzerModuleInfo.ModuleBase);
+ if (!(_hasScriptAnalyzerModule = VerifyPSScriptAnalyzerAvailable()))
+ {
+ throw new Exception("PSScriptAnalyzer module not available");
+ }
+
+ // Create a base session state with PSScriptAnalyzer loaded
+ InitialSessionState sessionState = InitialSessionState.CreateDefault2();
+ sessionState.ImportPSModule(new [] { PSSA_MODULE_NAME });
// runspacepool takes care of queuing commands for us so we do not
// need to worry about executing concurrent commands
- this.analysisRunspacePool = RunspaceFactory.CreateRunspacePool(sessionState);
+ this._analysisRunspacePool = RunspaceFactory.CreateRunspacePool(sessionState);
// having more than one runspace doesn't block code formatting if one
// runspace is occupied for diagnostics
- this.analysisRunspacePool.SetMaxRunspaces(NumRunspaces);
- this.analysisRunspacePool.ThreadOptions = PSThreadOptions.ReuseThread;
- this.analysisRunspacePool.Open();
+ this._analysisRunspacePool.SetMaxRunspaces(NumRunspaces);
+ this._analysisRunspacePool.ThreadOptions = PSThreadOptions.ReuseThread;
+ this._analysisRunspacePool.Open();
ActiveRules = IncludedRules.ToArray();
EnumeratePSScriptAnalyzerCmdlets();
@@ -137,7 +138,7 @@ public AnalysisService(string settingsPath, ILogger logger)
var sb = new StringBuilder();
sb.AppendLine("PSScriptAnalyzer cannot be imported, AnalysisService will be disabled.");
sb.AppendLine(e.Message);
- this.logger.Write(LogLevel.Warning, sb.ToString());
+ this._logger.Write(LogLevel.Warning, sb.ToString());
}
}
@@ -234,7 +235,7 @@ public async Task GetSemanticMarkersAsync(
public IEnumerable GetPSScriptAnalyzerRules()
{
List ruleNames = new List();
- if (hasScriptAnalyzerModule)
+ if (_hasScriptAnalyzerModule)
{
var ruleObjects = InvokePowerShell("Get-ScriptAnalyzerRule", new Dictionary());
foreach (var rule in ruleObjects)
@@ -259,7 +260,7 @@ public async Task Format(
int[] rangeList)
{
// we cannot use Range type therefore this workaround of using -1 default value
- if (!hasScriptAnalyzerModule)
+ if (!_hasScriptAnalyzerModule)
{
return null;
}
@@ -282,11 +283,11 @@ public async Task Format(
///
public void Dispose()
{
- if (this.analysisRunspacePool != null)
+ if (this._analysisRunspacePool != null)
{
- this.analysisRunspacePool.Close();
- this.analysisRunspacePool.Dispose();
- this.analysisRunspacePool = null;
+ this._analysisRunspacePool.Close();
+ this._analysisRunspacePool.Dispose();
+ this._analysisRunspacePool = null;
}
}
@@ -299,7 +300,7 @@ private async Task GetSemanticMarkersAsync(
string[] rules,
TSettings settings) where TSettings : class
{
- if (hasScriptAnalyzerModule
+ if (_hasScriptAnalyzerModule
&& file.IsAnalysisEnabled)
{
return await GetSemanticMarkersAsync(
@@ -332,44 +333,9 @@ private async Task GetSemanticMarkersAsync(
}
}
- private static PSModuleInfo FindPSScriptAnalyzerModule(ILogger logger)
- {
- using (var ps = System.Management.Automation.PowerShell.Create())
- {
- ps.AddCommand("Get-Module")
- .AddParameter("ListAvailable")
- .AddParameter("Name", "PSScriptAnalyzer");
-
- ps.AddCommand("Sort-Object")
- .AddParameter("Descending")
- .AddParameter("Property", "Version");
-
- ps.AddCommand("Select-Object")
- .AddParameter("First", 1);
-
- var modules = ps.Invoke();
- var psModuleInfo = modules == null ? null : modules.FirstOrDefault();
- if (psModuleInfo != null)
- {
- logger.Write(
- LogLevel.Normal,
- string.Format(
- "PSScriptAnalyzer found at {0}",
- psModuleInfo.Path));
-
- return psModuleInfo;
- }
-
- logger.Write(
- LogLevel.Normal,
- "PSScriptAnalyzer module was not found.");
- return null;
- }
- }
-
private void EnumeratePSScriptAnalyzerCmdlets()
{
- if (hasScriptAnalyzerModule)
+ if (_hasScriptAnalyzerModule)
{
var sb = new StringBuilder();
var commands = InvokePowerShell(
@@ -386,13 +352,13 @@ private void EnumeratePSScriptAnalyzerCmdlets()
sb.AppendLine("The following cmdlets are available in the imported PSScriptAnalyzer module:");
sb.AppendLine(String.Join(Environment.NewLine, commandNames.Select(s => " " + s)));
- this.logger.Write(LogLevel.Verbose, sb.ToString());
+ this._logger.Write(LogLevel.Verbose, sb.ToString());
}
}
private void EnumeratePSScriptAnalyzerRules()
{
- if (hasScriptAnalyzerModule)
+ if (_hasScriptAnalyzerModule)
{
var rules = GetPSScriptAnalyzerRules();
var sb = new StringBuilder();
@@ -402,7 +368,7 @@ private void EnumeratePSScriptAnalyzerRules()
sb.AppendLine(rule);
}
- this.logger.Write(LogLevel.Verbose, sb.ToString());
+ this._logger.Write(LogLevel.Verbose, sb.ToString());
}
}
@@ -421,7 +387,7 @@ private async Task GetDiagnosticRecordsAsync(
return diagnosticRecords;
}
- if (hasScriptAnalyzerModule
+ if (_hasScriptAnalyzerModule
&& (typeof(TSettings) == typeof(string)
|| typeof(TSettings) == typeof(Hashtable)))
{
@@ -448,7 +414,7 @@ private async Task GetDiagnosticRecordsAsync(
});
}
- this.logger.Write(
+ this._logger.Write(
LogLevel.Verbose,
String.Format("Found {0} violations", diagnosticRecords.Count()));
@@ -460,7 +426,7 @@ private PSObject[] InvokePowerShell(string command, IDictionary
{
using (var powerShell = System.Management.Automation.PowerShell.Create())
{
- powerShell.RunspacePool = this.analysisRunspacePool;
+ powerShell.RunspacePool = this._analysisRunspacePool;
powerShell.AddCommand(command);
foreach (var kvp in paramArgMap)
{
@@ -472,13 +438,19 @@ private PSObject[] InvokePowerShell(string command, IDictionary
{
result = powerShell.Invoke()?.ToArray();
}
+ catch (CommandNotFoundException ex)
+ {
+ // This exception is possible if the module path loaded
+ // is wrong even though PSScriptAnalyzer is available as a module
+ this._logger.Write(LogLevel.Error, ex.Message);
+ }
catch (CmdletInvocationException ex)
{
// We do not want to crash EditorServices for exceptions caused by cmdlet invocation.
// Two main reasons that cause the exception are:
// * PSCmdlet.WriteOutput being called from another thread than Begin/Process
// * CompositionContainer.ComposeParts complaining that "...Only one batch can be composed at a time"
- this.logger.Write(LogLevel.Error, ex.Message);
+ this._logger.Write(LogLevel.Error, ex.Message);
}
return result;
@@ -495,6 +467,25 @@ private async Task InvokePowerShellAsync(string command, IDictionary
return await task;
}
+ private bool VerifyPSScriptAnalyzerAvailable()
+ {
+ using (var ps = System.Management.Automation.PowerShell.Create())
+ {
+ ps.AddCommand("Get-Module")
+ .AddParameter("ListAvailable")
+ .AddParameter("Name", PSSA_MODULE_NAME);
+
+ try
+ {
+ return ps.Invoke()?.Any() ?? false;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+ }
+
#endregion //private methods
}
}
diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs
index 741b69abe..6ec39b431 100644
--- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs
+++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs
@@ -19,7 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Test.Host
{
public class ServerTestsBase
{
- private static int sessionCounter;
+ private static int sessionCounter;
private Process serviceProcess;
protected IMessageSender messageSender;
protected IMessageHandlers messageHandlers;
@@ -35,7 +35,12 @@ protected async Task> LaunchService(
bool waitForDebugger = false)
{
string modulePath = Path.GetFullPath(@"..\..\..\..\..\module");
- string scriptPath = Path.Combine(modulePath, "Start-EditorServices.ps1");
+ string scriptPath = Path.GetFullPath(Path.Combine(modulePath, @"PowerShellEditorServices\Start-EditorServices.ps1"));
+
+ if (!File.Exists(scriptPath))
+ {
+ throw new IOException(String.Format("Bad start script path: '{0}'", scriptPath));
+ }
#if CoreCLR
Assembly assembly = this.GetType().GetTypeInfo().Assembly;
@@ -47,7 +52,7 @@ protected async Task> LaunchService(
FileVersionInfo fileVersionInfo =
FileVersionInfo.GetVersionInfo(assemblyPath);
- string sessionPath =
+ string sessionPath =
Path.Combine(
Path.GetDirectoryName(assemblyPath), $"session-{++sessionCounter}.json");
@@ -64,9 +69,7 @@ protected async Task> LaunchService(
fileVersionInfo.FileBuildPart);
string scriptArgs =
- string.Format(
"\"" + scriptPath + "\" " +
- "-EditorServicesVersion \"{0}\" " +
"-HostName \\\"PowerShell Editor Services Test Host\\\" " +
"-HostProfileId \"Test.PowerShellEditorServices\" " +
"-HostVersion \"1.0.0\" " +
@@ -75,8 +78,7 @@ protected async Task> LaunchService(
"-LogPath \"" + logPath + "\" " +
"-SessionDetailsPath \"" + sessionPath + "\" " +
"-FeatureFlags @() " +
- "-AdditionalModules @() ",
- editorServicesModuleVersion);
+ "-AdditionalModules @() ";
if (waitForDebugger)
{
@@ -117,6 +119,11 @@ protected async Task> LaunchService(
var maxRetryAttempts = 10;
while (maxRetryAttempts-- > 0)
{
+ if (this.serviceProcess.HasExited)
+ {
+ throw new Exception(String.Format("Server host process quit unexpectedly: '{0}'", this.serviceProcess.StandardError.ReadToEnd()));
+ }
+
try
{
using (var stream = new FileStream(sessionPath, FileMode.Open, FileAccess.Read, FileShare.None))
@@ -126,9 +133,6 @@ protected async Task> LaunchService(
break;
}
}
- catch (FileNotFoundException)
- {
- }
catch (Exception ex)
{
Debug.WriteLine($"Session details at '{sessionPath}' not available: {ex.Message}");