diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 4dc22fc64..eacc30251 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -37,8 +37,17 @@ task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestP } # Make sure the dotnet we found is the right version - if ($dotnetExePath -and (& $dotnetExePath --version) -eq $requiredSdkVersion) { - $script:dotnetExe = $dotnetExePath + if ($dotnetExePath) { + # dotnet --version can return a semver that System.Version can't handle + # e.g.: 2.1.300-preview-01. The replace operator is used to remove any build suffix. + $version = (& $dotnetExePath --version) -replace '[+-].*$','' + if ([version]$version -ge [version]$requiredSdkVersion) { + $script:dotnetExe = $dotnetExePath + } + else { + # Clear the path so that we invoke installation + $script:dotnetExe = $null + } } else { # Clear the path so that we invoke installation @@ -78,7 +87,7 @@ task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestP $env:DOTNET_INSTALL_DIR = $dotnetExeDir } - Write-Host "`n### Using dotnet v$requiredSDKVersion at path $script:dotnetExe`n" -ForegroundColor Green + Write-Host "`n### Using dotnet v$(& $script:dotnetExe --version) at path $script:dotnetExe`n" -ForegroundColor Green } task Clean { diff --git a/module/Start-EditorServices.ps1 b/module/Start-EditorServices.ps1 index 332016352..a1f9152ab 100644 --- a/module/Start-EditorServices.ps1 +++ b/module/Start-EditorServices.ps1 @@ -44,9 +44,26 @@ param( [ValidateNotNullOrEmpty()] $LogPath, - [ValidateSet("Normal", "Verbose", "Error","Diagnostic")] + [ValidateSet("Diagnostic", "Normal", "Verbose", "Error", "Diagnostic")] $LogLevel, + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $SessionDetailsPath, + + [switch] + $EnableConsoleRepl, + + [switch] + $DebugServiceOnly, + + [string[]] + $AdditionalModules, + + [string[]] + $FeatureFlags, + [switch] $WaitForDebugger, @@ -54,89 +71,186 @@ param( $ConfirmInstall ) +$minPortNumber = 10000 +$maxPortNumber = 30000 + +if ($LogLevel -eq "Diagnostic") { + $VerbosePreference = 'Continue' + Start-Transcript (Join-Path (Split-Path $LogPath -Parent) Start-EditorServices.log) -Force +} + +function LogSection([string]$msg) { + Write-Verbose "`n#-- $msg $('-' * ([Math]::Max(0, 73 - $msg.Length)))" +} + +function Log([string[]]$msg) { + $msg | Write-Verbose +} + +function ExitWithError($errorString) { + Write-Host -ForegroundColor Red "`n`n$errorString" + + # Sleep for a while to make sure the user has time to see and copy the + # error message + Start-Sleep -Seconds 300 + + exit 1; +} + +# 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 + "{`"status`": `"failed`", `"reason`": `"unsupported`", `"powerShellVersion`": `"$($PSVersionTable.PSVersion.ToString())`"}" | + Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop + + 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 @{ + "status" = "failed" + "reason" = "languageMode" + "detail" = $host.Runspace.LanguageMode.ToString() + } + + ExitWithError "PowerShell is configured with an unsupported LanguageMode (ConstrainedLanguage), language features are disabled." +} + +# Are we running in PowerShell 5 or later? +$isPS5orLater = $PSVersionTable.PSVersion.Major -ge 5 + +# If PSReadline is present in the session, remove it so that runspace +# management is easier +if ((Get-Module PSReadline).Count -gt 0) { + LogSection "Removing PSReadLine module" + Remove-Module PSReadline -ErrorAction SilentlyContinue +} + # This variable will be assigned later to contain information about # what happened while attempting to launch the PowerShell Editor # Services host $resultDetails = $null; function Test-ModuleAvailable($ModuleName, $ModuleVersion) { + Log "Testing module availability $ModuleName $ModuleVersion" + $modules = Get-Module -ListAvailable $moduleName if ($modules -ne $null) { if ($ModuleVersion -ne $null) { foreach ($module in $modules) { if ($module.Version.Equals($moduleVersion)) { + Log "$ModuleName $ModuleVersion found" return $true; } } } else { + Log "$ModuleName $ModuleVersion found" return $true; } } + Log "$ModuleName $ModuleVersion NOT found" return $false; } -function Test-PortAvailability($PortNumber) { - $portAvailable = $true; +function Test-PortAvailability { + param( + [Parameter(Mandatory=$true)] + [int] + $PortNumber + ) + + $portAvailable = $true try { - $ipAddress = [System.Net.Dns]::GetHostEntryAsync("localhost").Result.AddressList[0]; - $tcpListener = [System.Net.Sockets.TcpListener]::new($ipAddress, $portNumber); - $tcpListener.Start(); - $tcpListener.Stop(); + if ($isPS5orLater) { + $ipAddresses = [System.Net.Dns]::GetHostEntryAsync("localhost").Result.AddressList + } + else { + $ipAddresses = [System.Net.Dns]::GetHostEntry("localhost").AddressList + } + foreach ($ipAddress in $ipAddresses) + { + Log "Testing availability of port ${PortNumber} at address ${ipAddress} / $($ipAddress.AddressFamily)" + + $tcpListener = New-Object System.Net.Sockets.TcpListener @($ipAddress, $PortNumber) + $tcpListener.Start() + $tcpListener.Stop() + } } catch [System.Net.Sockets.SocketException] { + $portAvailable = $false + # Check the SocketErrorCode to see if it's the expected exception - if ($error[0].Exception.InnerException.SocketErrorCode -eq [System.Net.Sockets.SocketError]::AddressAlreadyInUse) { - $portAvailable = $false; + if ($_.Exception.SocketErrorCode -eq [System.Net.Sockets.SocketError]::AddressAlreadyInUse) { + Log "Port $PortNumber is in use." } else { - Write-Output ("Error code: " + $error[0].SocketErrorCode) + Log "SocketException on port ${PortNumber}: $($_.Exception)" } } - return $portAvailable; + $portAvailable } -$rand = [System.Random]::new() -function Get-AvailablePort { +$portsInUse = @{} +$rand = New-Object System.Random +function Get-AvailablePort() { $triesRemaining = 10; while ($triesRemaining -gt 0) { - $port = $rand.Next(10000, 30000) - if ((Test-PortAvailability -PortAvailability $port) -eq $true) { + do { + $port = $rand.Next($minPortNumber, $maxPortNumber) + } + while ($portsInUse.ContainsKey($port)) + + # Whether we succeed or fail, don't try this port again + $portsInUse[$port] = 1 + + Log "Checking port: $port, attempts remaining $triesRemaining --------------------" + if ((Test-PortAvailability -PortNumber $port) -eq $true) { + Log "Port: $port is available" return $port } + Log "Port: $port is NOT available" $triesRemaining--; } + Log "Did not find any available ports!!" return $null } -# OUTPUT PROTOCOL -# - "started 29981 39898" - Server(s) are started, language and debug server ports (respectively) -# - "failed Error message describing the failure" - General failure while starting, show error message to user (?) -# - "needs_install" - User should be prompted to install PowerShell Editor Services via the PowerShell Gallery - # Add BundledModulesPath to $env:PSModulePath if ($BundledModulesPath) { - $env:PSMODULEPATH = $BundledModulesPath + [System.IO.Path]::PathSeparator + $env:PSMODULEPATH + $env:PSModulePath = $env:PSModulePath.TrimEnd([System.IO.Path]::PathSeparator) + [System.IO.Path]::PathSeparator + $BundledModulesPath + LogSection "Updated PSModulePath to:" + Log ($env:PSModulePath -split [System.IO.Path]::PathSeparator) } +LogSection "Check required modules available" # Check if PowerShellGet module is available if ((Test-ModuleAvailable "PowerShellGet") -eq $false) { + Log "Failed to find PowerShellGet module" # TODO: WRITE ERROR } # Check if the expected version of the PowerShell Editor Services # module is installed -$parsedVersion = [System.Version]::new($EditorServicesVersion) -if ((Test-ModuleAvailable "PowerShellEditorServices" -RequiredVersion $parsedVersion) -eq $false) { - if ($ConfirmInstall) { +$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 { @@ -146,49 +260,94 @@ if ((Test-ModuleAvailable "PowerShellEditorServices" -RequiredVersion $parsedVer } } -Import-Module PowerShellEditorServices -RequiredVersion $parsedVersion -ErrorAction Stop +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 + } + + # Locate available port numbers for services + Log "Searching for available socket port for the language service" + $languageServicePort = Get-AvailablePort + + Log "Searching for available socket port for the debug service" + $debugServicePort = Get-AvailablePort -# Locate available port numbers for services -$languageServicePort = Get-AvailablePort -$debugServicePort = Get-AvailablePort + if (!$languageServicePort -or !$debugServicePort) { + ExitWithError "Failed to find an open socket port for either the language or debug service." + } + + if ($EnableConsoleRepl) { + Write-Host "PowerShell Integrated Console`n" + } + + # Create the Editor Services host + Log "Invoking Start-EditorServicesHost" + $editorServicesHost = + Start-EditorServicesHost ` + -HostName $HostName ` + -HostProfileId $HostProfileId ` + -HostVersion $HostVersion ` + -LogPath $LogPath ` + -LogLevel $LogLevel ` + -AdditionalModules $AdditionalModules ` + -LanguageServicePort $languageServicePort ` + -DebugServicePort $debugServicePort ` + -BundledModulesPath $BundledModulesPath ` + -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` + -DebugServiceOnly:$DebugServiceOnly.IsPresent ` + -WaitForDebugger:$WaitForDebugger.IsPresent + + # TODO: Verify that the service is started + Log "Start-EditorServicesHost returned $editorServicesHost" + + $resultDetails = @{ + "status" = "started"; + "channel" = "tcp"; + "languageServicePort" = $languageServicePort; + "debugServicePort" = $debugServicePort; + } + + # Notify the client that the services have started + WriteSessionFile $resultDetails -$editorServicesHost = - Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules @() ` - -LanguageServicePort $languageServicePort ` - -DebugServicePort $debugServicePort ` - -BundledModulesPath $BundledModulesPath ` - -WaitForDebugger:$WaitForDebugger.IsPresent + Log "Wrote out session file" +} +catch [System.Exception] { + $e = $_.Exception; + $errorString = "" -# TODO: Verify that the service is started + Log "ERRORS caught starting up EditorServicesHost" -$resultDetails = @{ - "status" = "started"; - "channel" = "tcp"; - "languageServicePort" = $languageServicePort; - "debugServicePort" = $debugServicePort; -}; + while ($e -ne $null) { + $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") + $e = $e.InnerException; + Log $errorString + } -# Notify the client that the services have started -Write-Output (ConvertTo-Json -InputObject $resultDetails -Compress) + ExitWithError ("An error occurred while starting PowerShell Editor Services:`r`n`r`n" + $errorString) +} try { # Wait for the host to complete execution before exiting + LogSection "Waiting for EditorServicesHost to complete execution" $editorServicesHost.WaitForCompletion() + Log "EditorServicesHost has completed execution" } catch [System.Exception] { - $e = $_.Exception; #.InnerException; + $e = $_.Exception; $errorString = "" + Log "ERRORS caught while waiting for EditorServicesHost to complete execution" + while ($e -ne $null) { $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") $e = $e.InnerException; + Log $errorString } - - Write-Error ("`r`nCaught error while waiting for EditorServicesHost to complete:`r`n" + $errorString) } diff --git a/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs index 32bd199ff..eebb6095c 100644 --- a/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs @@ -27,8 +27,22 @@ IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( return Enumerable.Empty(); } - var commandAsts = scriptFile.ScriptAst.FindAll(ast => - { + // Find plausible Pester commands + IEnumerable commandAsts = scriptFile.ScriptAst.FindAll(IsNamedCommandWithArguments, true); + + return commandAsts.OfType() + .Where(IsPesterCommand) + .Select(ast => ConvertPesterAstToSymbolReference(scriptFile, ast)) + .Where(pesterSymbol => pesterSymbol?.TestName != null); + } + + /// + /// Test if the given Ast is a regular CommandAst with arguments + /// + /// the PowerShell Ast to test + /// true if the Ast represents a PowerShell command with arguments, false otherwise + private static bool IsNamedCommandWithArguments(Ast ast) + { CommandAst commandAst = ast as CommandAst; return @@ -36,54 +50,83 @@ IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( commandAst.InvocationOperator != TokenKind.Dot && PesterSymbolReference.GetCommandType(commandAst.GetCommandName()).HasValue && commandAst.CommandElements.Count >= 2; - }, - true); + } - return commandAsts.Select( - ast => - { - // By this point we know the Ast is a CommandAst with 2 or more CommandElements - int testNameParamIndex = 1; - CommandAst testAst = (CommandAst)ast; + /// + /// Test whether the given CommandAst represents a Pester command + /// + /// the CommandAst to test + /// true if the CommandAst represents a Pester command, false otherwise + private static bool IsPesterCommand(CommandAst commandAst) + { + if (commandAst == null) + { + return false; + } - // The -Name parameter - for (int i = 1; i < testAst.CommandElements.Count; i++) - { - CommandParameterAst paramAst = testAst.CommandElements[i] as CommandParameterAst; - if (paramAst != null && - paramAst.ParameterName.Equals("Name", StringComparison.OrdinalIgnoreCase)) - { - testNameParamIndex = i + 1; - break; - } - } + // Ensure the first word is a Pester keyword + if (!PesterSymbolReference.PesterKeywords.ContainsKey(commandAst.GetCommandName())) + { + return false; + } - if (testNameParamIndex > testAst.CommandElements.Count - 1) - { - return null; - } + // Ensure that the last argument of the command is a scriptblock + if (!(commandAst.CommandElements[commandAst.CommandElements.Count-1] is ScriptBlockExpressionAst)) + { + return false; + } + + return true; + } - StringConstantExpressionAst stringAst = - testAst.CommandElements[testNameParamIndex] as StringConstantExpressionAst; + /// + /// Convert a CommandAst known to represent a Pester command and a reference to the scriptfile + /// it is in into symbol representing a Pester call for code lens + /// + /// the scriptfile the Pester call occurs in + /// the CommandAst representing the Pester call + /// a symbol representing the Pester call containing metadata for CodeLens to use + private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFile scriptFile, CommandAst pesterCommandAst) + { + string testLine = scriptFile.GetLine(pesterCommandAst.Extent.StartLineNumber); + string commandName = pesterCommandAst.GetCommandName(); + + // Search for a name for the test + // If the test has more than one argument for names, we set it to null + string testName = null; + bool alreadySawName = false; + for (int i = 1; i < pesterCommandAst.CommandElements.Count; i++) + { + CommandElementAst currentCommandElement = pesterCommandAst.CommandElements[i]; - if (stringAst == null) + // Check for an explicit "-Name" parameter + if (currentCommandElement is CommandParameterAst parameterAst) + { + i++; + if (parameterAst.ParameterName == "Name" && i < pesterCommandAst.CommandElements.Count) { - return null; + testName = alreadySawName ? null : (pesterCommandAst.CommandElements[i] as StringConstantExpressionAst)?.Value; + alreadySawName = true; } + continue; + } - string testDefinitionLine = - scriptFile.GetLine( - ast.Extent.StartLineNumber); - - return - new PesterSymbolReference( - scriptFile, - testAst.GetCommandName(), - testDefinitionLine, - stringAst.Value, - ast.Extent); + // Otherwise, if an argument is given with no parameter, we assume it's the name + // If we've already seen a name, we set the name to null + if (pesterCommandAst.CommandElements[i] is StringConstantExpressionAst testNameStrAst) + { + testName = alreadySawName ? null : testNameStrAst.Value; + alreadySawName = true; + } + } - }).Where(s => s != null); + return new PesterSymbolReference( + scriptFile, + commandName, + testLine, + testName, + pesterCommandAst.Extent + ); } } @@ -114,6 +157,14 @@ public enum PesterCommandType /// public class PesterSymbolReference : SymbolReference { + /// + /// Lookup for Pester keywords we support. Ideally we could extract these from Pester itself + /// + internal static readonly IReadOnlyDictionary PesterKeywords = + Enum.GetValues(typeof(PesterCommandType)) + .Cast() + .ToDictionary(pct => pct.ToString(), pct => pct); + private static char[] DefinitionTrimChars = new char[] { ' ', '{' }; /// @@ -145,25 +196,12 @@ internal PesterSymbolReference( internal static PesterCommandType? GetCommandType(string commandName) { - if (commandName == null) + PesterCommandType pesterCommandType; + if (!PesterKeywords.TryGetValue(commandName, out pesterCommandType)) { return null; } - - switch (commandName.ToLower()) - { - case "describe": - return PesterCommandType.Describe; - - case "context": - return PesterCommandType.Context; - - case "it": - return PesterCommandType.It; - - default: - return null; - } + return pesterCommandType; } } } diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 3b5749f5e..7b192cfed 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -390,22 +390,30 @@ internal static bool IsPathInMemory(string filePath) // view of the current file or an untitled file. try { - // NotSupportedException or ArgumentException gets thrown when - // given an invalid path. Since any non-standard path will - // trigger this, assume that it means it's an in-memory file - // unless the path starts with file: - Path.GetFullPath(filePath); + // File system absoulute paths will have a URI scheme of file:. + // Other schemes like "untitled:" and "gitlens-git:" will return false for IsFile. + var uri = new Uri(filePath); + isInMemory = !uri.IsFile; } - catch (ArgumentException) + catch (UriFormatException) { - isInMemory = true; - } - catch (NotSupportedException) - { - isInMemory = true; + // Relative file paths cause a UriFormatException. + // In this case, fallback to using Path.GetFullPath(). + try + { + Path.GetFullPath(filePath); + } + catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException) + { + isInMemory = true; + } + catch (PathTooLongException) + { + // If we ever get here, it should be an actual file so, not in memory + } } - return !filePath.ToLower().StartsWith("file:") && isInMemory; + return isInMemory; } private string GetBaseFilePath(string filePath) diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index eeaac47cb..b4d9fbd6b 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -825,7 +825,9 @@ await this.SendRequest( Assert.StartsWith("5.", versionDetails.Version); Assert.StartsWith("5.", versionDetails.DisplayVersion); Assert.Equal("Desktop", versionDetails.Edition); - Assert.Equal("x86", versionDetails.Architecture); + + string expectedArchitecture = (IntPtr.Size == 8) ? "x64" : "x86"; + Assert.Equal(expectedArchitecture, versionDetails.Architecture); } private async Task SendOpenFileEvent(string filePath, bool waitForDiagnostics = true) diff --git a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj index c3344c0d1..7aec43019 100644 --- a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj +++ b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj @@ -22,6 +22,7 @@ 6.0.0-alpha13 + diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index 6cfb1f6dc..741b69abe 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -10,13 +10,16 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.IO; +using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Test.Host { public class ServerTestsBase { + private static int sessionCounter; private Process serviceProcess; protected IMessageSender messageSender; protected IMessageHandlers messageHandlers; @@ -35,13 +38,24 @@ protected async Task> LaunchService( string scriptPath = Path.Combine(modulePath, "Start-EditorServices.ps1"); #if CoreCLR - FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); + Assembly assembly = this.GetType().GetTypeInfo().Assembly; #else - FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().Assembly.Location); + Assembly assembly = this.GetType().Assembly; #endif + string assemblyPath = new Uri(assembly.CodeBase).LocalPath; + FileVersionInfo fileVersionInfo = + FileVersionInfo.GetVersionInfo(assemblyPath); + + string sessionPath = + Path.Combine( + Path.GetDirectoryName(assemblyPath), $"session-{++sessionCounter}.json"); + + if (File.Exists(sessionPath)) + { + File.Delete(sessionPath); + } + string editorServicesModuleVersion = string.Format( "{0}.{1}.{2}", @@ -58,7 +72,10 @@ protected async Task> LaunchService( "-HostVersion \"1.0.0\" " + "-BundledModulesPath \\\"" + modulePath + "\\\" " + "-LogLevel \"Verbose\" " + - "-LogPath \"" + logPath + "\" ", + "-LogPath \"" + logPath + "\" " + + "-SessionDetailsPath \"" + sessionPath + "\" " + + "-FeatureFlags @() " + + "-AdditionalModules @() ", editorServicesModuleVersion); if (waitForDebugger) @@ -94,41 +111,43 @@ protected async Task> LaunchService( // Start the process this.serviceProcess.Start(); - // Wait for the server to finish initializing - Task stdoutTask = this.serviceProcess.StandardOutput.ReadLineAsync(); - Task stderrTask = this.serviceProcess.StandardError.ReadLineAsync(); - Task completedRead = await Task.WhenAny(stdoutTask, stderrTask); + string sessionDetailsText = string.Empty; - if (completedRead == stdoutTask) + // Wait up to ~5 seconds for the server to finish initializing + var maxRetryAttempts = 10; + while (maxRetryAttempts-- > 0) { - JObject result = JObject.Parse(completedRead.Result); - if (result["status"].Value() == "started") + try + { + using (var stream = new FileStream(sessionPath, FileMode.Open, FileAccess.Read, FileShare.None)) + using (var reader = new StreamReader(stream)) + { + sessionDetailsText = reader.ReadToEnd(); + break; + } + } + catch (FileNotFoundException) + { + } + catch (Exception ex) { - return new Tuple( - result["languageServicePort"].Value(), - result["debugServicePort"].Value()); + Debug.WriteLine($"Session details at '{sessionPath}' not available: {ex.Message}"); } - return null; + Thread.Sleep(500); } - else + + JObject result = JObject.Parse(sessionDetailsText); + if (result["status"].Value() == "started") { - // Must have read an error? Keep reading from error stream - string errorString = completedRead.Result; - Task errorRead = this.serviceProcess.StandardError.ReadToEndAsync(); + return new Tuple( + result["languageServicePort"].Value(), + result["debugServicePort"].Value()); + } - // Lets give the read operation 5 seconds to complete. Ideally, it shouldn't - // take that long at all, but just in case... - if (errorRead.Wait(5000)) - { - if (!string.IsNullOrEmpty(errorRead.Result)) - { - errorString += errorRead.Result + Environment.NewLine; - } - } + Debug.WriteLine($"Failed to read session details from '{sessionPath}'"); - throw new Exception("Could not launch powershell.exe:\r\n\r\n" + errorString); - } + return null; } protected void KillService() diff --git a/test/PowerShellEditorServices.Test.Protocol/PowerShellEditorServices.Test.Protocol.csproj b/test/PowerShellEditorServices.Test.Protocol/PowerShellEditorServices.Test.Protocol.csproj index 7b8db1e84..9f10745c8 100644 --- a/test/PowerShellEditorServices.Test.Protocol/PowerShellEditorServices.Test.Protocol.csproj +++ b/test/PowerShellEditorServices.Test.Protocol/PowerShellEditorServices.Test.Protocol.csproj @@ -12,6 +12,7 @@ + 9.0.1 diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/PesterFile.tests.ps1 b/test/PowerShellEditorServices.Test.Shared/Symbols/PesterFile.tests.ps1 index 086b071e0..f44d607e2 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/PesterFile.tests.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/PesterFile.tests.ps1 @@ -13,3 +13,15 @@ } } } + +Describe -Tags "Tag1", "Tag2" "Another dummy test" { + It "Works as expected" +} + +Describe "A third test" -Tag "Tag3" { + Context "Given pester tags" { + It "Should still detect Pester symbols" { + $true | Should -BeExactly $true + } + } +} diff --git a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs index 1b4c05e17..c90990e8e 100644 --- a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs @@ -305,7 +305,7 @@ public void LanguageServiceFindsSymbolsInFile() public void LanguageServiceFindsSymbolsInPesterFile() { var symbolsResult = this.FindSymbolsInFile(FindSymbolsInPesterFile.SourceDetails); - Assert.Equal(5, symbolsResult.FoundOccurrences.Count()); + Assert.Equal(10, symbolsResult.FoundOccurrences.Count()); } [Fact] diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj index 8eb7f18df..199cd3b52 100644 --- a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj +++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj @@ -15,6 +15,7 @@ 6.0.0-alpha13 + diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index 4dfdabdc9..41679285f 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -3,12 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices; using System; using System.IO; -using System.Linq; -using Xunit; using Microsoft.PowerShell.EditorServices.Utility; +using Xunit; namespace Microsoft.PowerShell.EditorServices.Test.Session { @@ -35,5 +33,41 @@ public void CanResolveWorkspaceRelativePath() Assert.Equal(@"..\PeerPath\FilePath.ps1", workspace.GetRelativePath(testPathOutside)); Assert.Equal(testPathAnotherDrive, workspace.GetRelativePath(testPathAnotherDrive)); } + + [Fact] + public void CanDetermineIsPathInMemory() + { + var tempDir = Environment.GetEnvironmentVariable("TEMP"); + var shortDirPath = Path.Combine(tempDir, "GitHub", "PowerShellEditorServices"); + var shortFilePath = Path.Combine(shortDirPath, "foo.ps1"); + var shortUriForm = "git:/c%3A/Users/Keith/GitHub/dahlbyk/posh-git/src/PoshGitTypes.ps1?%7B%22path%22%3A%22c%3A%5C%5CUsers%5C%5CKeith%5C%5CGitHub%5C%5Cdahlbyk%5C%5Cposh-git%5C%5Csrc%5C%5CPoshGitTypes.ps1%22%2C%22ref%22%3A%22~%22%7D"; + var longUriForm = "gitlens-git:c%3A%5CUsers%5CKeith%5CGitHub%5Cdahlbyk%5Cposh-git%5Csrc%5CPoshGitTypes%3Ae0022701.ps1?%7B%22fileName%22%3A%22src%2FPoshGitTypes.ps1%22%2C%22repoPath%22%3A%22c%3A%2FUsers%2FKeith%2FGitHub%2Fdahlbyk%2Fposh-git%22%2C%22sha%22%3A%22e0022701fa12e0bc22d0458673d6443c942b974a%22%7D"; + + var testCases = new[] { + // Test short file absolute paths + new { IsInMemory = false, Path = shortDirPath }, + new { IsInMemory = false, Path = shortFilePath }, + new { IsInMemory = false, Path = new Uri(shortDirPath).ToString() }, + new { IsInMemory = false, Path = new Uri(shortFilePath).ToString() }, + + // Test short file relative paths - not sure we'll ever get these but just in case + new { IsInMemory = false, Path = "foo.ps1" }, + new { IsInMemory = false, Path = ".." + Path.DirectorySeparatorChar + "foo.ps1" }, + + // Test short non-file paths + new { IsInMemory = true, Path = "untitled:untitled-1" }, + new { IsInMemory = true, Path = shortUriForm }, + + // Test long non-file path - known to have crashed PSES + new { IsInMemory = true, Path = longUriForm }, + }; + + foreach (var testCase in testCases) + { + Assert.True( + Workspace.IsPathInMemory(testCase.Path) == testCase.IsInMemory, + $"Testing path {testCase.Path}"); + } + } } }