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}");