From c708a1bfeab40f8d5ff69349d3c1148e35d0036b Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 19 Apr 2019 11:23:36 -0700 Subject: [PATCH 01/46] Add first parts of ps pses client --- test/Pester/EditorServices.Tests.ps1 | 2 + test/Pester/tools/PsesIntegration.psm1 | 357 +++++++++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 test/Pester/EditorServices.Tests.ps1 create mode 100644 test/Pester/tools/PsesIntegration.psm1 diff --git a/test/Pester/EditorServices.Tests.ps1 b/test/Pester/EditorServices.Tests.ps1 new file mode 100644 index 000000000..0df56029a --- /dev/null +++ b/test/Pester/EditorServices.Tests.ps1 @@ -0,0 +1,2 @@ +Describe "Loading and running PowerShellEditorServices" { +} diff --git a/test/Pester/tools/PsesIntegration.psm1 b/test/Pester/tools/PsesIntegration.psm1 new file mode 100644 index 000000000..705dc4801 --- /dev/null +++ b/test/Pester/tools/PsesIntegration.psm1 @@ -0,0 +1,357 @@ +class NamedPipeWrapper +{ + NamedPipeWrapper( + [System.IO.Pipes.NamedPipeClientStream] + $namedPipeClient + ) + { + $this.NamedPipeClient = $namedPipeClient + } + + hidden [System.IO.Pipes.NamedPipeClientStream] $NamedPipeClient + + hidden [System.IO.StreamReader] $Reader + + hidden [System.IO.StreamWriter] $Writer + + Connect() + { + $this.NamedPipeClient.Connect() + + $encoding = [System.Text.UTF8Encoding]::new($false) + $this.Reader = [System.IO.StreamReader]::new($this.NamedPipeClient, $encoding) + $this.Writer = [System.IO.StreamWriter]::new($this.NamedPipeClient, $encoding) + $this.Writer.AutoFlush = $true + } + + Send([string]$message) + { + $this.Writer.Write($message) + } + + Dispose() + { + $this.Reader.Dispose() + $this.Writer.Dispose() + $this.NamedPipeClient.Dispose() + } +} + +$script:PsesBundledModulesDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( + "$PSScriptRoot/../../../PowerShellEditorServices") + +Import-Module "$script:PsesBundledModulesDir/PowerShellEditorServices" + +function Start-PsesServer +{ + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $EditorServicesPath = "$script:PsesBundledModulesDir/PowerShellEditorServices/Start-EditorServices.ps1", + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $LogPath, + + [Parameter()] + [ValidateSet("Diagnostic", "Normal", "Verbose", "Error")] + [string] + $LogLevel = 'Diagnostic', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $SessionDetailsPath, + + [Parameter()] + [ValidateNotNull()] + [string[]] + $FeatureFlags = @('PSReadLine'), + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $HostName = 'PSES Test Host', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $HostProfileId = 'TestHost', + + [Parameter()] + [ValidateNotNull()] + [version] + $HostVersion = '1.99', + + [Parameter()] + [ValidateNotNull()] + [string[]] + $AdditionalModules = @('PowerShellEditorServices.VSCode'), + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $BundledModulesPath, + + [Parameter()] + [switch] + $EnableConsoleRepl + ) + + $EditorServicesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($EditorServicesPath) + + $instanceId = Get-RandomHexString + + $tempDir = [System.IO.Path]::GetTempPath() + + if (-not $LogPath) + { + $LogPath = Join-Path $tempDir "pseslogs_$instanceId.log" + } + + if (-not $SessionDetailsPath) + { + $SessionDetailsPath = Join-Path $tempDir "psessession_$instanceId.log" + } + + if (-not $BundledModulesPath) + { + $BundledModulesPath = $script:PsesBundledModulesDir + } + + $editorServicesOptions = @{ + LogPath = $LogPath + LogLevel = $LogLevel + SessionDetailsPath = $SessionDetailsPath + FeatureFlags = $FeatureFlags + HostName = $HostName + HostProfileId = $HostProfileId + HostVersion = $HostVersion + AdditionalModules = $AdditionalModules + BundledModulesPath = $BundledModulesPath + EnableConsoleRepl = $EnableConsoleRepl + } + + $startPsesCommand = Unsplat -Prefix "& '$EditorServicesPath'" -SplatParams $editorServicesOptions + + $pwshPath = (Get-Process -Id $PID).Path + + if (-not $PSCmdlet.ShouldProcess("& '$pwshPath' -Command '$startPsesCommand'")) + { + return + } + + $serverProcess = Start-Process -PassThru -FilePath $pwshPath -ArgumentList @( + '-NoLogo', + '-NoProfile', + '-NoExit', + '-Command', + $startPsesCommand + ) + + $sessionPath = $editorServicesOptions.SessionDetailsPath + + $i = 0 + while (-not (Test-Path $sessionPath)) + { + if ($i -ge 10) + { + throw "No session file found - server failed to start" + } + + Start-Sleep 1 + $null = $i++ + } + + return @{ + Process = $serverProcess + SessionDetails = Get-Content -Raw $editorServicesOptions.SessionDetailsPath | ConvertFrom-Json + StartupOptions = $editorServicesOptions + } +} + +function Connect-NamedPipe +{ + param( + [Parameter(Mandatory)] + [string] + $PipeName + ) + + Wait-Debugger + + $pipe = [NamedPipeWrapper]::new(([System.IO.Pipes.NamedPipeClientStream]::new('.', $PipeName, 'InOut'))) + $pipe.Connect() + return $pipe +} + +function Send-LspInitializeRequest +{ + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter()] + [NamedPipeWrapper] + $Pipe, + + [Parameter()] + [int] + $ProcessId = $PID, + + [Parameter()] + [string] + $RootPath = (Get-Location), + + [Parameter()] + [string] + $RootUri, + + [Parameter()] + [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ClientCapabilities] + $ClientCapabilities = ([Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ClientCapabilities]::new()), + + [Parameter()] + [hashtable] + $InitializeOptions = $null + ) + + $parameters = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.InitializeParams]@{ + ProcessId = $ProcessId + Capabilities = $ClientCapabilities + InitializeOptions = $InitializeOptions + } + + if ($RootUri) + { + $parameters.RootUri = $RootUri + } + else + { + $parameters.RootPath = $RootPath + } + + Send-LspRequest -Pipe $Pipe -Method 'initialize' -Parameters $parameters -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') +} + +$script:MessageId = -1 +$script:JsonSerializerSettings = [Newtonsoft.Json.JsonSerializerSettings]@{ + ContractResolver = [Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver]::new() +} +$script:Utf8Encoding = [System.Text.UTF8Encoding]::new($false) +$script:JsonSerializer = [Newtonsoft.Json.JsonSerializer]::Create($script:JsonSerializerSettings) +$script:JsonRpcSerializer = [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers.JsonRpcMessageSerializer]::new() +function Send-LspRequest +{ + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter()] + [NamedPipeWrapper] + $Pipe, + + [Parameter()] + [string] + $Method, + + [Parameter()] + [object] + $Parameters + ) + + $null = $script:MessageId++ + + $msg = [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Message]::Request( + $script:MessageId, + $Method, + [Newtonsoft.Json.Linq.JToken]::FromObject($Parameters, $JsonSerializer)) + + $msgJson = $script:JsonRpcSerializer.SerializeMessage($msg) + $msgString = [Newtonsoft.Json.JsonConvert]::SerializeObject($msgJson, $script:JsonSerializerSettings) + $msgBytes = $script:Utf8Encoding.GetBytes($msgString) + + $header = "Content-Length: $($msgBytes.Length)`r`n`r`n" + $headerBytes = $script:Utf8Encoding.GetBytes($header) + + $bytesToSend = $headerBytes + $msgBytes + + if (-not $PSCmdlet.ShouldProcess("Send '$Method' message to server")) + { + return $script:Utf8Encoding.GetString($bytesToSend) + } + + $Pipe.Write($bytesToSend) +} + +function Unsplat +{ + param( + [string]$Prefix, + [hashtable]$SplatParams) + + $sb = New-Object 'System.Text.StringBuilder' ($Prefix) + + foreach ($key in $SplatParams.get_Keys()) + { + $val = $SplatParams[$key] + + if (-not $val) + { + continue + } + + $null = $sb.Append(" -$key") + + if ($val -is [switch]) + { + continue + } + + if ($val -is [array]) + { + $null = $sb.Append(' @(') + for ($i = 0; $i -lt $val.Count; $i++) + { + $null = $sb.Append("'").Append($val[$i]).Append("'") + if ($i -lt $val.Count - 1) + { + $null = $sb.Append(',') + } + } + $null = $sb.Append(')') + continue + } + + if ($val -is [version]) + { + $val = [string]$val + } + + if ($val -is [string]) + { + $null = $sb.Append(" '$val'") + continue + } + + throw "Bad value '$val' of type $($val.GetType())" + } + + return $sb.ToString() +} + +$script:Random = [System.Random]::new() +function Get-RandomHexString +{ + param([int]$Length = 10) + + $buffer = [byte[]]::new($Length / 2) + $script:Random.NextBytes($buffer) + $str = ($buffer | % { "{0:x02}" -f $_ }) -join '' + + if ($Length % 2 -ne 0) + { + $str += ($script:Random.Next() | % { "{0:02}" -f $_ }) + } + + return $str +} From e36001a710525a694e42501a3288baf3d34d29a7 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 19 Apr 2019 12:26:40 -0700 Subject: [PATCH 02/46] Make message reading work --- test/Pester/tools/PsesIntegration.psm1 | 65 ++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/test/Pester/tools/PsesIntegration.psm1 b/test/Pester/tools/PsesIntegration.psm1 index 705dc4801..e5f7812db 100644 --- a/test/Pester/tools/PsesIntegration.psm1 +++ b/test/Pester/tools/PsesIntegration.psm1 @@ -14,21 +14,76 @@ class NamedPipeWrapper hidden [System.IO.StreamWriter] $Writer + hidden [char[]] $ReaderBuffer + Connect() { - $this.NamedPipeClient.Connect() + $this.NamedPipeClient.Connect(1000) $encoding = [System.Text.UTF8Encoding]::new($false) $this.Reader = [System.IO.StreamReader]::new($this.NamedPipeClient, $encoding) $this.Writer = [System.IO.StreamWriter]::new($this.NamedPipeClient, $encoding) $this.Writer.AutoFlush = $true + $this.ReaderBuffer = [char[]]::new(1024) } - Send([string]$message) + Write([string]$message) { $this.Writer.Write($message) } + [bool] + HasContent() + { + return $this.Reader.Peek() -gt 0 + } + + [string] + ReadMessage() + { + $sb = [System.Text.StringBuilder]::new() + + $charCount = $this.Reader.Peek() + $remainingMessageChars = -1 + while ($remainingMessageChars -ne 0 -and $charCount -gt 0) + { + if ($charCount -gt $this.ReaderBuffer.Length) + { + [array]::Resize([ref]$this.ReaderBuffer, $this.ReaderBuffer.Length * 2) + } + + $this.Reader.Read($this.ReaderBuffer, 0, $charCount) + + $sb.Append($this.ReaderBuffer, 0, $charCount) + + if ($remainingMessageChars -ge 0) + { + $remainingMessageChars -= $charCount + } + else + { + $msgSoFar = $sb.ToString() + $endHeaderIdx = $msgSoFar.IndexOf("`r`n`r`n") + + if ($endHeaderIdx -ge 0) + { + $remainingMessageChars = [int]($msgSoFar.Substring(16, $endHeaderIdx - 16)) + + $overflowLength = $charCount - ($endHeaderIdx + 4) + + if ($overflowLength -gt 0) + { + $remainingMessageChars -= $overflowLength + } + } + } + + $charCount = [System.Math]::Min($remainingMessageChars, $this.Reader.Peek()) + } + + return $sb.ToString() + } + Dispose() { $this.Reader.Dispose() @@ -181,7 +236,11 @@ function Connect-NamedPipe $PipeName ) - Wait-Debugger + $psesIdx = $PipeName.IndexOf('PSES') + if ($psesIdx -gt 0) + { + $PipeName = $PipeName.Substring($psesIdx) + } $pipe = [NamedPipeWrapper]::new(([System.IO.Pipes.NamedPipeClientStream]::new('.', $PipeName, 'InOut'))) $pipe.Connect() From 9e67deab1cdc732cab41740b463c23400b2cb222 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 22 Apr 2019 11:45:05 +1000 Subject: [PATCH 03/46] Start pipe module --- test/Pester/tools/PsesIntegration.psm1 | 130 +++++++++------ tools/PsesPsClient/LspPipe.cs | 220 +++++++++++++++++++++++++ tools/PsesPsClient/PsesPsClient.csproj | 16 ++ 3 files changed, 312 insertions(+), 54 deletions(-) create mode 100644 tools/PsesPsClient/LspPipe.cs create mode 100644 tools/PsesPsClient/PsesPsClient.csproj diff --git a/test/Pester/tools/PsesIntegration.psm1 b/test/Pester/tools/PsesIntegration.psm1 index e5f7812db..21ff11771 100644 --- a/test/Pester/tools/PsesIntegration.psm1 +++ b/test/Pester/tools/PsesIntegration.psm1 @@ -6,8 +6,24 @@ class NamedPipeWrapper ) { $this.NamedPipeClient = $namedPipeClient + + $this.ReaderBuffer = [char[]]::new(1024) + + $this.MessageId = -1 + + $this.JsonSerializerSettings = [Newtonsoft.Json.JsonSerializerSettings]@{ + ContractResolver = [Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver]::new() + } + + $this.JsonSerializer = [Newtonsoft.Json.JsonSerializer]::Create($this.JsonSerializerSettings) + + $this.JsonRpcSerializer = [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers.JsonRpcMessageSerializer]::new() + + $this.PipeEncoding = [System.Text.UTF8Encoding]::new($false) } + [bool] $Debug + hidden [System.IO.Pipes.NamedPipeClientStream] $NamedPipeClient hidden [System.IO.StreamReader] $Reader @@ -16,6 +32,16 @@ class NamedPipeWrapper hidden [char[]] $ReaderBuffer + hidden [int] $MessageId + + hidden [Newtonsoft.Json.JsonSerializerSettings] $JsonSerializerSettings + + hidden [Newtonsoft.Json.JsonSerializer] $JsonSerializer + + hidden [System.Text.Encoding] $PipeEncoding + + hidden [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers.JsonRpcMessageSerializer] $JsonRpcSerializer + Connect() { $this.NamedPipeClient.Connect(1000) @@ -24,12 +50,35 @@ class NamedPipeWrapper $this.Reader = [System.IO.StreamReader]::new($this.NamedPipeClient, $encoding) $this.Writer = [System.IO.StreamWriter]::new($this.NamedPipeClient, $encoding) $this.Writer.AutoFlush = $true - $this.ReaderBuffer = [char[]]::new(1024) } - Write([string]$message) + Write([string]$method, [object]$parameters) { - $this.Writer.Write($message) + $this.MessageId++ + + $msg = [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Message]::Request( + $script:MessageId, + $Method, + [Newtonsoft.Json.Linq.JToken]::FromObject($Parameters, $this.JsonSerializer)) + + $msgJson = $this.JsonRpcSerializer.SerializeMessage($msg) + $msgString = [Newtonsoft.Json.JsonConvert]::SerializeObject($msgJson, $this.JsonSerializerSettings) + $msgBytes = $this.PipeEncoding.GetBytes($msgString) + + $header = "Content-Length: $($msgBytes.Length)`r`n`r`n" + $headerBytes = $script:Utf8Encoding.GetBytes($header) + + $bytesToSend = $headerBytes + $msgBytes + + $stringToSend = $this.PipeEncoding.GetBytes($bytesToSend) + + if ($this.Debug) + { + Write-Debug "Sending pipe message: $stringToSend" + return + } + + $this.Writer.Write($stringToSend) } [bool] @@ -38,9 +87,31 @@ class NamedPipeWrapper return $this.Reader.Peek() -gt 0 } - [string] + [object] ReadMessage() { + # Read the headers to get the content-length + $charCount = $this.Reader.Peek() + $charsRead = 0 + contentLength: while ($charCount -gt 0) + { + if ($charCount + $charsRead -gt $this.ReaderBuffer.Length) + { + [array]::Resize([ref]$this.ReaderBuffer.Length, $this.ReaderBuffer.Length * 2) + } + + $this.Reader.Read($this.ReaderBuffer, $charsRead, $charCount) + + for ($i = 0; $i -lt $this.ReaderBuffer.Length - 3; $i++) + { + if ($this.ReaderBuffer[$i] -eq 0xD -and + $this.ReaderBuffer[$i+1] -eq 0xA -and + $this.ReaderBuffer[$i] + } + + $charsRead += $charCount + } + $sb = [System.Text.StringBuilder]::new() $charCount = $this.Reader.Peek() @@ -249,7 +320,6 @@ function Connect-NamedPipe function Send-LspInitializeRequest { - [CmdletBinding(SupportsShouldProcess)] param( [Parameter()] [NamedPipeWrapper] @@ -291,55 +361,7 @@ function Send-LspInitializeRequest $parameters.RootPath = $RootPath } - Send-LspRequest -Pipe $Pipe -Method 'initialize' -Parameters $parameters -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -} - -$script:MessageId = -1 -$script:JsonSerializerSettings = [Newtonsoft.Json.JsonSerializerSettings]@{ - ContractResolver = [Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver]::new() -} -$script:Utf8Encoding = [System.Text.UTF8Encoding]::new($false) -$script:JsonSerializer = [Newtonsoft.Json.JsonSerializer]::Create($script:JsonSerializerSettings) -$script:JsonRpcSerializer = [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers.JsonRpcMessageSerializer]::new() -function Send-LspRequest -{ - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter()] - [NamedPipeWrapper] - $Pipe, - - [Parameter()] - [string] - $Method, - - [Parameter()] - [object] - $Parameters - ) - - $null = $script:MessageId++ - - $msg = [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Message]::Request( - $script:MessageId, - $Method, - [Newtonsoft.Json.Linq.JToken]::FromObject($Parameters, $JsonSerializer)) - - $msgJson = $script:JsonRpcSerializer.SerializeMessage($msg) - $msgString = [Newtonsoft.Json.JsonConvert]::SerializeObject($msgJson, $script:JsonSerializerSettings) - $msgBytes = $script:Utf8Encoding.GetBytes($msgString) - - $header = "Content-Length: $($msgBytes.Length)`r`n`r`n" - $headerBytes = $script:Utf8Encoding.GetBytes($header) - - $bytesToSend = $headerBytes + $msgBytes - - if (-not $PSCmdlet.ShouldProcess("Send '$Method' message to server")) - { - return $script:Utf8Encoding.GetString($bytesToSend) - } - - $Pipe.Write($bytesToSend) + $Pipe.Write('initialize', $parameters) } function Unsplat diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs new file mode 100644 index 000000000..19f6835f8 --- /dev/null +++ b/tools/PsesPsClient/LspPipe.cs @@ -0,0 +1,220 @@ +using System; +using System.IO.Pipes; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; +using System.Text; +using System.IO; +using Newtonsoft.Json.Linq; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using System.Collections.Generic; + +namespace PsesPsClient +{ + public class LspPipe : IDisposable + { + private static readonly IReadOnlyDictionary s_messageBodyTypes = new Dictionary() + { + + }; + + private readonly NamedPipeClientStream _namedPipeClient; + + private readonly StringBuilder _headerBuffer; + + private readonly JsonSerializerSettings _jsonSettings; + + private readonly JsonSerializer _jsonSerializer; + + private readonly JsonRpcMessageSerializer _jsonRpcSerializer; + + private readonly Encoding _pipeEncoding; + + private int _msgId; + + private StreamReader _reader; + + private StreamWriter _writer; + + private char[] _readerBuffer; + + public LspPipe(NamedPipeClientStream namedPipeClient) + { + _namedPipeClient = namedPipeClient; + + _readerBuffer = new char[1024]; + + _headerBuffer = new StringBuilder(128); + + _jsonSettings = new JsonSerializerSettings() + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + _jsonSerializer = JsonSerializer.Create(_jsonSettings); + + _jsonRpcSerializer = new JsonRpcMessageSerializer(); + + _pipeEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + } + + public bool HasContent + { + get + { + return _reader.Peek() > 0; + } + } + + public void Connect() + { + _namedPipeClient.Connect(timeout: 1000); + _reader = new StreamReader(_namedPipeClient, _pipeEncoding); + _writer = new StreamWriter(_namedPipeClient, _pipeEncoding) + { + AutoFlush = true + }; + } + + public void Write( + string method, + object parameters) + { + _msgId++; + + Message msg = Message.Request( + _msgId.ToString(), + method, + JToken.FromObject(parameters, _jsonSerializer)); + + JObject msgJson = _jsonRpcSerializer.SerializeMessage(msg); + string msgString = JsonConvert.SerializeObject(msgJson, _jsonSettings); + byte[] msgBytes = _pipeEncoding.GetBytes(msgString); + + string header = "Content-Length: " + msgBytes.Length + "\r\n\r\n"; + + _writer.Write(header); + _writer.Write(msgBytes); + } + + public LspMessage Read() + { + int contentLength = GetContentLength(); + string msgString = ReadString(contentLength); + JObject msgJson = JObject.Parse(msgString); + + if (!msgJson.TryGetValue("method", out JToken methodJsonToken) + || !(methodJsonToken is JValue methodJsonValue) + || !(methodJsonValue.Value is string method)) + { + throw new Exception($"No method given on message: '{msgString}'"); + } + + if (!s_messageBodyTypes.TryGetValue(method, out Type bodyType)) + { + throw new Exception($"Unknown message method: '{method}'"); + } + + int id = (int)msgJson["id"]; + + object body = msgJson["params"].ToObject(bodyType); + + return new LspMessage(id, method, body); + } + + public void Dispose() + { + _namedPipeClient.Dispose(); + } + + private string ReadString(int bytesToRead) + { + if (bytesToRead > _readerBuffer.Length) + { + Array.Resize(ref _readerBuffer, _readerBuffer.Length * 2); + } + + _reader.Read(_readerBuffer, 0, bytesToRead); + + return new string(_readerBuffer); + } + + private int GetContentLength() + { + _headerBuffer.Clear(); + int endHeaderState = 0; + int currChar; + while ((currChar = _reader.Read()) >= 0) + { + char c = (char)currChar; + _headerBuffer.Append(c); + switch (c) + { + case '\r': + if (endHeaderState == 2) + { + endHeaderState = 3; + continue; + } + endHeaderState = 1; + continue; + + case '\n': + if (endHeaderState == 1) + { + endHeaderState = 2; + continue; + } + + // This is the end, my only friend, the end + if (endHeaderState == 3) + { + return ParseContentLength(_headerBuffer.ToString()); + } + + continue; + } + } + + throw new Exception("Buffer emptied before end of headers"); + } + + private static int ParseContentLength(string headers) + { + const string clHeaderPrefix = "Content-Length: "; + + int clIdx = headers.IndexOf(clHeaderPrefix); + if (clIdx < 0) + { + throw new Exception("No Content-Length header found"); + } + + int endIdx = headers.IndexOf("\r\n", clIdx); + if (endIdx < 0) + { + throw new Exception("Header CRLF terminator not found"); + } + + int numStartIdx = clIdx + clHeaderPrefix.Length; + int numLength = endIdx - numStartIdx; + + return int.Parse(headers.Substring(numStartIdx, numLength)); + } + } + + public class LspMessage + { + public LspMessage(int id, string method, object body) + { + Id = id; + Method = method; + Body = body; + } + + public int Id { get; } + + public string Method { get; } + + public object Body; + } +} diff --git a/tools/PsesPsClient/PsesPsClient.csproj b/tools/PsesPsClient/PsesPsClient.csproj new file mode 100644 index 000000000..ab2c9f6ac --- /dev/null +++ b/tools/PsesPsClient/PsesPsClient.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + From b66a9ac27e44298f38d367761a2791ae834d68c5 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 22 Apr 2019 17:11:35 +1000 Subject: [PATCH 04/46] Add simple startup test --- global.json | 5 - test/Pester/EditorServices.Tests.ps1 | 18 + tools/PsesPsClient/.vscode/launch.json | 42 +++ tools/PsesPsClient/LspPipe.cs | 315 +++++++++++++++--- .../PsesPsClient/PsesPsClient.psm1 | 175 +--------- tools/PsesPsClient/build.ps1 | 11 + 6 files changed, 341 insertions(+), 225 deletions(-) delete mode 100644 global.json create mode 100644 tools/PsesPsClient/.vscode/launch.json rename test/Pester/tools/PsesIntegration.psm1 => tools/PsesPsClient/PsesPsClient.psm1 (54%) create mode 100644 tools/PsesPsClient/build.ps1 diff --git a/global.json b/global.json deleted file mode 100644 index 80bff6046..000000000 --- a/global.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sdk": { - "version": "2.1.602" - } -} diff --git a/test/Pester/EditorServices.Tests.ps1 b/test/Pester/EditorServices.Tests.ps1 index 0df56029a..325d4b40d 100644 --- a/test/Pester/EditorServices.Tests.ps1 +++ b/test/Pester/EditorServices.Tests.ps1 @@ -1,2 +1,20 @@ Describe "Loading and running PowerShellEditorServices" { + BeforeAll { + Import-Module "$PSScriptRoot/../../tools/PsesPsClient" + + $psesServer = Start-PsesServer + $pipe = Connect-NamedPipe -PipeName $psesServer.SessionDetails.languageServicePipeName + } + + AfterAll { + $pipe.Dispose() + $psesServer.Process.Close() + } + + It "Starts and responds to an initialization request" { + $request = Send-LspInitializeRequest -Pipe $pipe + $response = $null + $pipe.TryGetNextResponse([ref]$response, 5000) | Should -BeTrue + $response.Id | Should -BeExactly $request.Id + } } diff --git a/tools/PsesPsClient/.vscode/launch.json b/tools/PsesPsClient/.vscode/launch.json new file mode 100644 index 000000000..8b60d4fab --- /dev/null +++ b/tools/PsesPsClient/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "WARNING01": "*********************************************************************************", + "WARNING02": "The C# extension was unable to automatically to decode projects in the current", + "WARNING03": "workspace to create a runnable lanch.json file. A template launch.json file has", + "WARNING04": "been created as a placeholder.", + "WARNING05": "", + "WARNING06": "If OmniSharp is currently unable to load your project, you can attempt to resolve", + "WARNING07": "this by restoring any missing project dependencies (example: run 'dotnet restore')", + "WARNING08": "and by fixing any reported errors from building the projects in your workspace.", + "WARNING09": "If this allows OmniSharp to now load your project then --", + "WARNING10": " * Delete this file", + "WARNING11": " * Open the Visual Studio Code command palette (View->Command Palette)", + "WARNING12": " * run the command: '.NET: Generate Assets for Build and Debug'.", + "WARNING13": "", + "WARNING14": "If your project requires a more complex launch configuration, you may wish to delete", + "WARNING15": "this configuration and pick a different template using the 'Add Configuration...'", + "WARNING16": "button at the bottom of this file.", + "WARNING17": "*********************************************************************************", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug//.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index 19f6835f8..794369250 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -8,20 +8,29 @@ using Newtonsoft.Json.Linq; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using System.Collections.Generic; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Threading; +using System.Linq; namespace PsesPsClient { public class LspPipe : IDisposable { - private static readonly IReadOnlyDictionary s_messageBodyTypes = new Dictionary() + public static LspPipe Create(string pipeName) { + var pipeClient = new NamedPipeClientStream( + serverName: ".", + pipeName: pipeName, + direction: PipeDirection.InOut, + options: PipeOptions.Asynchronous); - }; + return new LspPipe(pipeClient); + } private readonly NamedPipeClientStream _namedPipeClient; - private readonly StringBuilder _headerBuffer; - private readonly JsonSerializerSettings _jsonSettings; private readonly JsonSerializer _jsonSerializer; @@ -32,20 +41,14 @@ public class LspPipe : IDisposable private int _msgId; - private StreamReader _reader; - private StreamWriter _writer; - private char[] _readerBuffer; + private MessageStreamListener _listener; public LspPipe(NamedPipeClientStream namedPipeClient) { _namedPipeClient = namedPipeClient; - _readerBuffer = new char[1024]; - - _headerBuffer = new StringBuilder(128); - _jsonSettings = new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver() @@ -58,25 +61,19 @@ public LspPipe(NamedPipeClientStream namedPipeClient) _pipeEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); } - public bool HasContent - { - get - { - return _reader.Peek() > 0; - } - } - public void Connect() { _namedPipeClient.Connect(timeout: 1000); - _reader = new StreamReader(_namedPipeClient, _pipeEncoding); + _listener = new MessageStreamListener(new StreamReader(_namedPipeClient, _pipeEncoding)); _writer = new StreamWriter(_namedPipeClient, _pipeEncoding) { AutoFlush = true }; + + _listener.Start(); } - public void Write( + public LspRequest WriteRequest( string method, object parameters) { @@ -93,50 +90,160 @@ public void Write( string header = "Content-Length: " + msgBytes.Length + "\r\n\r\n"; - _writer.Write(header); - _writer.Write(msgBytes); + _writer.Write(header + msgString); + _writer.Flush(); + + return new LspRequest(msg.Id, method, msgJson["params"]); } - public LspMessage Read() + public IEnumerable GetNotifications() { - int contentLength = GetContentLength(); - string msgString = ReadString(contentLength); - JObject msgJson = JObject.Parse(msgString); + return _listener.DrainNotifications(); + } - if (!msgJson.TryGetValue("method", out JToken methodJsonToken) - || !(methodJsonToken is JValue methodJsonValue) - || !(methodJsonValue.Value is string method)) - { - throw new Exception($"No method given on message: '{msgString}'"); - } + public IEnumerable GetRequests() + { + return _listener.DrainRequests(); + } - if (!s_messageBodyTypes.TryGetValue(method, out Type bodyType)) - { - throw new Exception($"Unknown message method: '{method}'"); - } + public bool TryGetNextResponse(out LspResponse response, int millisTimeout) + { + return _listener.TryGetNextResponse(out response, millisTimeout); + } + + public void Dispose() + { + _listener.Dispose(); + _namedPipeClient.Dispose(); + } + } + + public class MessageStreamListener : IDisposable + { + private readonly StreamReader _stream; + + private readonly StringBuilder _headerBuffer; - int id = (int)msgJson["id"]; + private readonly ConcurrentQueue _requestQueue; - object body = msgJson["params"].ToObject(bodyType); + private readonly ConcurrentQueue _notificationQueue; - return new LspMessage(id, method, body); + private readonly BlockingCollection _responseBlockingOutput; + + private char[] _readerBuffer; + + private CancellationTokenSource _cancellationSource; + + public MessageStreamListener(StreamReader stream) + { + _stream = stream; + _readerBuffer = new char[1024]; + _headerBuffer = new StringBuilder(128); + _notificationQueue = new ConcurrentQueue(); + _requestQueue = new ConcurrentQueue(); + _responseBlockingOutput = new BlockingCollection(); + _cancellationSource = new CancellationTokenSource(); + } + + public IEnumerable DrainNotifications() + { + return DrainQueue(_notificationQueue); + } + + public IEnumerable DrainRequests() + { + return DrainQueue(_requestQueue); + } + + public bool TryGetNextResponse(out LspResponse response) + { + return _responseBlockingOutput.TryTake(out response); + } + + public bool TryGetNextResponse(out LspResponse response, int millisTimeout) + { + return _responseBlockingOutput.TryTake(out response, millisTimeout); + } + + public void Start() + { + Task.Run(() => RunListenLoop()); + } + + public void Stop() + { + _cancellationSource.Cancel(); } public void Dispose() { - _namedPipeClient.Dispose(); + Stop(); + _stream.Dispose(); } - private string ReadString(int bytesToRead) + private async Task RunListenLoop() + { + while (!_cancellationSource.IsCancellationRequested) + { + LspMessage msg = await ReadMessage(); + switch (msg) + { + case LspNotification notification: + _notificationQueue.Enqueue(notification); + continue; + + case LspResponse response: + _responseBlockingOutput.Add(response); + continue; + + case LspRequest request: + _requestQueue.Enqueue(request); + continue; + } + } + } + + private async Task ReadMessage() + { + int contentLength = GetContentLength(); + string msgString = await ReadString(contentLength); + JObject msgJson = JObject.Parse(msgString); + + if (msgJson.TryGetValue("method", out JToken methodToken)) + { + string method = ((JValue)methodToken).Value.ToString(); + if (msgJson.TryGetValue("id", out JToken idToken)) + { + string requestId = ((JValue)idToken).Value.ToString(); + return new LspRequest(requestId, method, msgJson["params"]); + } + + return new LspNotification(method, msgJson["params"]); + } + + string id = ((JValue)msgJson["id"]).Value.ToString(); + + if (msgJson.TryGetValue("result", out JToken resultToken)) + { + return new LspSuccessfulResponse(id, resultToken); + } + + JObject errorBody = (JObject)msgJson["error"]; + JsonRpcErrorCode errorCode = (JsonRpcErrorCode)(int)((JValue)errorBody["code"]).Value; + string message = (string)((JValue)errorBody["message"]).Value; + return new LspErrorResponse(id, errorCode, message, errorBody["data"]); + } + + private async Task ReadString(int bytesToRead) { if (bytesToRead > _readerBuffer.Length) { Array.Resize(ref _readerBuffer, _readerBuffer.Length * 2); } - _reader.Read(_readerBuffer, 0, bytesToRead); + int readLen = await _stream.ReadAsync(_readerBuffer, 0, bytesToRead); - return new string(_readerBuffer); + return new string(_readerBuffer, 0, readLen); } private int GetContentLength() @@ -144,7 +251,7 @@ private int GetContentLength() _headerBuffer.Clear(); int endHeaderState = 0; int currChar; - while ((currChar = _reader.Read()) >= 0) + while ((currChar = _stream.Read()) >= 0) { char c = (char)currChar; _headerBuffer.Append(c); @@ -156,7 +263,14 @@ private int GetContentLength() endHeaderState = 3; continue; } - endHeaderState = 1; + + if (endHeaderState == 0) + { + endHeaderState = 1; + continue; + } + + endHeaderState = 0; continue; case '\n': @@ -172,6 +286,11 @@ private int GetContentLength() return ParseContentLength(_headerBuffer.ToString()); } + endHeaderState = 0; + continue; + + default: + endHeaderState = 0; continue; } } @@ -200,21 +319,115 @@ private static int ParseContentLength(string headers) return int.Parse(headers.Substring(numStartIdx, numLength)); } + + private static IEnumerable DrainQueue(ConcurrentQueue queue) + { + if (queue.IsEmpty) + { + return Enumerable.Empty(); + } + + var list = new List(); + while (queue.TryDequeue(out TElement element)) + { + list.Add(element); + } + return list; + } + + } + + public abstract class LspMessage + { + protected LspMessage() + { + } + } + + public class LspNotification : LspMessage + { + public LspNotification(string method, JToken parameters) + : base() + { + Method = method; + Params = parameters; + } + + public string Method { get; } + + public JToken Params { get; } } - public class LspMessage + public class LspRequest : LspMessage { - public LspMessage(int id, string method, object body) + public LspRequest(string id, string method, JToken parameters) { Id = id; Method = method; - Body = body; + Params = parameters; } - public int Id { get; } + public string Id { get; } public string Method { get; } - public object Body; + public JToken Params { get; } + } + + public abstract class LspResponse : LspMessage + { + protected LspResponse(string id) + { + Id = id; + } + + public string Id { get; } + } + + public class LspSuccessfulResponse : LspResponse + { + public LspSuccessfulResponse(string id, JToken result) + : base(id) + { + Result = result; + } + + public JToken Result { get; } + } + + public class LspErrorResponse : LspResponse + { + public LspErrorResponse( + string id, + JsonRpcErrorCode code, + string message, + JToken data) + : base(id) + { + Code = code; + Message = message; + Data = data; + } + + public JsonRpcErrorCode Code { get; } + + public string Message { get; } + + public JToken Data { get; } + } + + public enum JsonRpcErrorCode : int + { + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + ServerErrorStart = -32099, + ServerErrorEnd = -32000, + ServerNotInitialized = -32002, + UnknownErrorCode = -32001, + RequestCancelled = -32800, + ContentModified = -32801, } } diff --git a/test/Pester/tools/PsesIntegration.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 similarity index 54% rename from test/Pester/tools/PsesIntegration.psm1 rename to tools/PsesPsClient/PsesPsClient.psm1 index 21ff11771..6c4916dcb 100644 --- a/test/Pester/tools/PsesIntegration.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -1,173 +1,10 @@ -class NamedPipeWrapper -{ - NamedPipeWrapper( - [System.IO.Pipes.NamedPipeClientStream] - $namedPipeClient - ) - { - $this.NamedPipeClient = $namedPipeClient - - $this.ReaderBuffer = [char[]]::new(1024) - - $this.MessageId = -1 - - $this.JsonSerializerSettings = [Newtonsoft.Json.JsonSerializerSettings]@{ - ContractResolver = [Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver]::new() - } - - $this.JsonSerializer = [Newtonsoft.Json.JsonSerializer]::Create($this.JsonSerializerSettings) - - $this.JsonRpcSerializer = [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers.JsonRpcMessageSerializer]::new() - - $this.PipeEncoding = [System.Text.UTF8Encoding]::new($false) - } - - [bool] $Debug - - hidden [System.IO.Pipes.NamedPipeClientStream] $NamedPipeClient - - hidden [System.IO.StreamReader] $Reader - - hidden [System.IO.StreamWriter] $Writer - - hidden [char[]] $ReaderBuffer - - hidden [int] $MessageId - - hidden [Newtonsoft.Json.JsonSerializerSettings] $JsonSerializerSettings - - hidden [Newtonsoft.Json.JsonSerializer] $JsonSerializer - - hidden [System.Text.Encoding] $PipeEncoding - - hidden [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers.JsonRpcMessageSerializer] $JsonRpcSerializer - - Connect() - { - $this.NamedPipeClient.Connect(1000) - - $encoding = [System.Text.UTF8Encoding]::new($false) - $this.Reader = [System.IO.StreamReader]::new($this.NamedPipeClient, $encoding) - $this.Writer = [System.IO.StreamWriter]::new($this.NamedPipeClient, $encoding) - $this.Writer.AutoFlush = $true - } - - Write([string]$method, [object]$parameters) - { - $this.MessageId++ - - $msg = [Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Message]::Request( - $script:MessageId, - $Method, - [Newtonsoft.Json.Linq.JToken]::FromObject($Parameters, $this.JsonSerializer)) - - $msgJson = $this.JsonRpcSerializer.SerializeMessage($msg) - $msgString = [Newtonsoft.Json.JsonConvert]::SerializeObject($msgJson, $this.JsonSerializerSettings) - $msgBytes = $this.PipeEncoding.GetBytes($msgString) - - $header = "Content-Length: $($msgBytes.Length)`r`n`r`n" - $headerBytes = $script:Utf8Encoding.GetBytes($header) - - $bytesToSend = $headerBytes + $msgBytes - - $stringToSend = $this.PipeEncoding.GetBytes($bytesToSend) - - if ($this.Debug) - { - Write-Debug "Sending pipe message: $stringToSend" - return - } - - $this.Writer.Write($stringToSend) - } - - [bool] - HasContent() - { - return $this.Reader.Peek() -gt 0 - } - - [object] - ReadMessage() - { - # Read the headers to get the content-length - $charCount = $this.Reader.Peek() - $charsRead = 0 - contentLength: while ($charCount -gt 0) - { - if ($charCount + $charsRead -gt $this.ReaderBuffer.Length) - { - [array]::Resize([ref]$this.ReaderBuffer.Length, $this.ReaderBuffer.Length * 2) - } - - $this.Reader.Read($this.ReaderBuffer, $charsRead, $charCount) - - for ($i = 0; $i -lt $this.ReaderBuffer.Length - 3; $i++) - { - if ($this.ReaderBuffer[$i] -eq 0xD -and - $this.ReaderBuffer[$i+1] -eq 0xA -and - $this.ReaderBuffer[$i] - } - - $charsRead += $charCount - } - - $sb = [System.Text.StringBuilder]::new() - - $charCount = $this.Reader.Peek() - $remainingMessageChars = -1 - while ($remainingMessageChars -ne 0 -and $charCount -gt 0) - { - if ($charCount -gt $this.ReaderBuffer.Length) - { - [array]::Resize([ref]$this.ReaderBuffer, $this.ReaderBuffer.Length * 2) - } - - $this.Reader.Read($this.ReaderBuffer, 0, $charCount) - - $sb.Append($this.ReaderBuffer, 0, $charCount) - - if ($remainingMessageChars -ge 0) - { - $remainingMessageChars -= $charCount - } - else - { - $msgSoFar = $sb.ToString() - $endHeaderIdx = $msgSoFar.IndexOf("`r`n`r`n") - - if ($endHeaderIdx -ge 0) - { - $remainingMessageChars = [int]($msgSoFar.Substring(16, $endHeaderIdx - 16)) - - $overflowLength = $charCount - ($endHeaderIdx + 4) - - if ($overflowLength -gt 0) - { - $remainingMessageChars -= $overflowLength - } - } - } - - $charCount = [System.Math]::Min($remainingMessageChars, $this.Reader.Peek()) - } - - return $sb.ToString() - } - - Dispose() - { - $this.Reader.Dispose() - $this.Writer.Dispose() - $this.NamedPipeClient.Dispose() - } -} - $script:PsesBundledModulesDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( - "$PSScriptRoot/../../../PowerShellEditorServices") + "$PSScriptRoot/../../module") Import-Module "$script:PsesBundledModulesDir/PowerShellEditorServices" +Import-Module $PSScriptRoot/bin/Debug/netstandard2.0/PsesPsClient.dll + function Start-PsesServer { [CmdletBinding(SupportsShouldProcess)] @@ -313,7 +150,7 @@ function Connect-NamedPipe $PipeName = $PipeName.Substring($psesIdx) } - $pipe = [NamedPipeWrapper]::new(([System.IO.Pipes.NamedPipeClientStream]::new('.', $PipeName, 'InOut'))) + $pipe = [PsesPsClient.LspPipe]::Create($PipeName) $pipe.Connect() return $pipe } @@ -322,7 +159,7 @@ function Send-LspInitializeRequest { param( [Parameter()] - [NamedPipeWrapper] + [PsesPsClient.LspPipe] $Pipe, [Parameter()] @@ -361,7 +198,7 @@ function Send-LspInitializeRequest $parameters.RootPath = $RootPath } - $Pipe.Write('initialize', $parameters) + return $Pipe.WriteRequest('initialize', $parameters) } function Unsplat diff --git a/tools/PsesPsClient/build.ps1 b/tools/PsesPsClient/build.ps1 new file mode 100644 index 000000000..8873c767f --- /dev/null +++ b/tools/PsesPsClient/build.ps1 @@ -0,0 +1,11 @@ +param( + [switch] + $Clean +) + +if ($Clean) +{ + Remove-Item -Force -Recurse "$PSScriptRoot/bin","$PSScriptRoot/obj" +} + +dotnet build From 6711016d4722d9b21c54430d3ba134ab499dfaa1 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 22 Apr 2019 18:45:02 +1000 Subject: [PATCH 05/46] Add shutdown test --- PowerShellEditorServices.build.ps1 | 12 +++++++++++- global.json | 5 +++++ test/Pester/EditorServices.Tests.ps1 | 8 ++++++++ tools/PsesPsClient/LspPipe.cs | 2 +- tools/PsesPsClient/PsesPsClient.psm1 | 15 +++++++++++++-- 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 global.json diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 9991125b5..da58c7314 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -329,12 +329,16 @@ task Build { exec { & $script:dotnetExe build -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj $script:TargetFrameworksParam } } +task BuildPsesClientModule { + & $PSScriptRoot/tools/PsesPsClient/build.ps1 -Clean +} + function DotNetTestFilter { # Reference https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests if ($TestFilter) { @("--filter",$TestFilter) } else { "" } } -task Test TestServer,TestProtocol +task Test TestServer,TestProtocol,TestPester task TestServer { Set-Location .\test\PowerShellEditorServices.Test\ @@ -372,6 +376,12 @@ task TestHost { exec { & $script:dotnetExe test -f $script:TestRuntime.Core (DotNetTestFilter) } } +task TestPester -After Build,BuildPsesClientModule { + $pwshExe = (Get-Process -Id $PID).Path + $pesterTestDir = Resolve-Path "$PSScriptRoot/test/Pester/" + exec { & $pwshExe -Command "cd $pesterTestDir; Invoke-Pester" } +} + task LayoutModule -After Build { # Copy Third Party Notices.txt to module folder Copy-Item -Force -Path "$PSScriptRoot\Third Party Notices.txt" -Destination $PSScriptRoot\module\PowerShellEditorServices diff --git a/global.json b/global.json new file mode 100644 index 000000000..80bff6046 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "2.1.602" + } +} diff --git a/test/Pester/EditorServices.Tests.ps1 b/test/Pester/EditorServices.Tests.ps1 index 325d4b40d..d94f71337 100644 --- a/test/Pester/EditorServices.Tests.ps1 +++ b/test/Pester/EditorServices.Tests.ps1 @@ -17,4 +17,12 @@ Describe "Loading and running PowerShellEditorServices" { $pipe.TryGetNextResponse([ref]$response, 5000) | Should -BeTrue $response.Id | Should -BeExactly $request.Id } + + It "Shuts down the process properly" { + $request = Send-LspShutdownRequest -Pipe $pipe + $response = $null + $pipe.TryGetNextResponse([ref]$response, 5000) | Should -BeTrue + $response.Id | Should -BeExactly $request.Id + $response.Result.Type | Should -Be 'Null' + } } diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index 794369250..ef5302ac8 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -82,7 +82,7 @@ public LspRequest WriteRequest( Message msg = Message.Request( _msgId.ToString(), method, - JToken.FromObject(parameters, _jsonSerializer)); + parameters != null ? JToken.FromObject(parameters, _jsonSerializer) : JValue.CreateNull()); JObject msgJson = _jsonRpcSerializer.SerializeMessage(msg); string msgString = JsonConvert.SerializeObject(msgJson, _jsonSettings); diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 6c4916dcb..e6f17af0d 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -158,7 +158,7 @@ function Connect-NamedPipe function Send-LspInitializeRequest { param( - [Parameter()] + [Parameter(Position = 0, Mandatory)] [PsesPsClient.LspPipe] $Pipe, @@ -201,6 +201,17 @@ function Send-LspInitializeRequest return $Pipe.WriteRequest('initialize', $parameters) } +function Send-LspShutdownRequest +{ + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.LspPipe] + $Pipe + ) + + $Pipe.WriteRequest('shutdown', $null) +} + function Unsplat { param( @@ -264,7 +275,7 @@ function Get-RandomHexString $buffer = [byte[]]::new($Length / 2) $script:Random.NextBytes($buffer) - $str = ($buffer | % { "{0:x02}" -f $_ }) -join '' + $str = ($buffer | ForEach-Object { "{0:x02}" -f $_ }) -join '' if ($Length % 2 -ne 0) { From be4587e78ec24aa32bb9485cac6e9fb672da488d Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 08:45:16 +1000 Subject: [PATCH 06/46] Add pester installation to build step --- PowerShellEditorServices.build.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index da58c7314..01565b008 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -376,12 +376,21 @@ task TestHost { exec { & $script:dotnetExe test -f $script:TestRuntime.Core (DotNetTestFilter) } } -task TestPester -After Build,BuildPsesClientModule { +task TestPester -After Build,BuildPsesClientModule,EnsurePesterInstalled { $pwshExe = (Get-Process -Id $PID).Path $pesterTestDir = Resolve-Path "$PSScriptRoot/test/Pester/" exec { & $pwshExe -Command "cd $pesterTestDir; Invoke-Pester" } } +task EnsurePesterInstalled { + if (Get-Command Invoke-Pester -ErrorAction SilentlyContinue) + { + return + } + + Install-Module -Scope CurrentUser Pester +} + task LayoutModule -After Build { # Copy Third Party Notices.txt to module folder Copy-Item -Force -Path "$PSScriptRoot\Third Party Notices.txt" -Destination $PSScriptRoot\module\PowerShellEditorServices From 2158e3b28ded199a02fbee8f7f2353488fe34749 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 08:48:59 +1000 Subject: [PATCH 07/46] Response to Codacy bot's feedback --- tools/PsesPsClient/LspPipe.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index ef5302ac8..7cc3da998 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -132,7 +132,7 @@ public class MessageStreamListener : IDisposable private char[] _readerBuffer; - private CancellationTokenSource _cancellationSource; + private readonly CancellationTokenSource _cancellationSource; public MessageStreamListener(StreamReader stream) { @@ -185,7 +185,7 @@ private async Task RunListenLoop() { while (!_cancellationSource.IsCancellationRequested) { - LspMessage msg = await ReadMessage(); + LspMessage msg = await ReadMessage().ConfigureAwait(false); switch (msg) { case LspNotification notification: @@ -206,7 +206,7 @@ private async Task RunListenLoop() private async Task ReadMessage() { int contentLength = GetContentLength(); - string msgString = await ReadString(contentLength); + string msgString = await ReadString(contentLength).ConfigureAwait(false); JObject msgJson = JObject.Parse(msgString); if (msgJson.TryGetValue("method", out JToken methodToken)) @@ -241,7 +241,7 @@ private async Task ReadString(int bytesToRead) Array.Resize(ref _readerBuffer, _readerBuffer.Length * 2); } - int readLen = await _stream.ReadAsync(_readerBuffer, 0, bytesToRead); + int readLen = await _stream.ReadAsync(_readerBuffer, 0, bytesToRead).ConfigureAwait(false); return new string(_readerBuffer, 0, readLen); } @@ -347,7 +347,6 @@ protected LspMessage() public class LspNotification : LspMessage { public LspNotification(string method, JToken parameters) - : base() { Method = method; Params = parameters; From 4952bad35b2aa7bb6d3f40e3260cf4a54d54509a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 09:08:32 +1000 Subject: [PATCH 08/46] Add classes and types to psm1 --- tools/PsesPsClient/PsesPsClient.psm1 | 30 +++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index e6f17af0d..85fd19751 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -5,9 +5,33 @@ Import-Module "$script:PsesBundledModulesDir/PowerShellEditorServices" Import-Module $PSScriptRoot/bin/Debug/netstandard2.0/PsesPsClient.dll +class PsesStartupOptions +{ + [string] $LogPath + [string] $LogLevel + [string] $SessionDetailsPath + [string[]] $FeatureFlags + [string] $HostName + [string] $HostProfileId + [version] $HostVersion + [string[]] $AdditionalModules + [string] $BundledModulesPath + [bool] $EnableConsoleRepl +} + +class PsesServerInfo +{ + [pscustomobject]$SessionDetails + + [System.Diagnostics.Process]$PsesProcess + + [PsesStartupOptions]$StartupOptions +} + function Start-PsesServer { [CmdletBinding(SupportsShouldProcess)] + [OutputType([PsesServerInfo])] param( [Parameter()] [ValidateNotNullOrEmpty()] @@ -129,8 +153,8 @@ function Start-PsesServer $null = $i++ } - return @{ - Process = $serverProcess + return [PsesServerInfo]@{ + PsesProcess = $serverProcess SessionDetails = Get-Content -Raw $editorServicesOptions.SessionDetailsPath | ConvertFrom-Json StartupOptions = $editorServicesOptions } @@ -279,7 +303,7 @@ function Get-RandomHexString if ($Length % 2 -ne 0) { - $str += ($script:Random.Next() | % { "{0:02}" -f $_ }) + $str += ($script:Random.Next() | ForEach-Object { "{0:02}" -f $_ }) } return $str From 7ba21c27d7972272f949185f425c049eed3cef4f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 10:01:03 +1000 Subject: [PATCH 09/46] Fix build logic, add return types --- PowerShellEditorServices.build.ps1 | 2 +- test/Pester/EditorServices.Tests.ps1 | 4 +++- tools/PsesPsClient/PsesPsClient.psm1 | 3 +++ tools/PsesPsClient/build.ps1 | 20 ++++++++++++++++++-- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 01565b008..bcd0290b0 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -376,7 +376,7 @@ task TestHost { exec { & $script:dotnetExe test -f $script:TestRuntime.Core (DotNetTestFilter) } } -task TestPester -After Build,BuildPsesClientModule,EnsurePesterInstalled { +task TestPester Build,BuildPsesClientModule,EnsurePesterInstalled,{ $pwshExe = (Get-Process -Id $PID).Path $pesterTestDir = Resolve-Path "$PSScriptRoot/test/Pester/" exec { & $pwshExe -Command "cd $pesterTestDir; Invoke-Pester" } diff --git a/test/Pester/EditorServices.Tests.ps1 b/test/Pester/EditorServices.Tests.ps1 index d94f71337..b62fb2183 100644 --- a/test/Pester/EditorServices.Tests.ps1 +++ b/test/Pester/EditorServices.Tests.ps1 @@ -23,6 +23,8 @@ Describe "Loading and running PowerShellEditorServices" { $response = $null $pipe.TryGetNextResponse([ref]$response, 5000) | Should -BeTrue $response.Id | Should -BeExactly $request.Id - $response.Result.Type | Should -Be 'Null' + $response.Result | Should -BeNull + # TODO: The server stays up waiting for the debug connection + # $psesServer.PsesProcess.HasExited | Should -BeTrue } } diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 85fd19751..7e2e6a53d 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -162,6 +162,7 @@ function Start-PsesServer function Connect-NamedPipe { + [OutputType([PsesPsClient.LspPipe])] param( [Parameter(Mandatory)] [string] @@ -181,6 +182,7 @@ function Connect-NamedPipe function Send-LspInitializeRequest { + [OutputType([PsesPsClient.LspRequest])] param( [Parameter(Position = 0, Mandatory)] [PsesPsClient.LspPipe] @@ -227,6 +229,7 @@ function Send-LspInitializeRequest function Send-LspShutdownRequest { + [OutputType([PsesPsClient.LspRequest])] param( [Parameter(Position = 0, Mandatory)] [PsesPsClient.LspPipe] diff --git a/tools/PsesPsClient/build.ps1 b/tools/PsesPsClient/build.ps1 index 8873c767f..7558d64f7 100644 --- a/tools/PsesPsClient/build.ps1 +++ b/tools/PsesPsClient/build.ps1 @@ -5,7 +5,23 @@ param( if ($Clean) { - Remove-Item -Force -Recurse "$PSScriptRoot/bin","$PSScriptRoot/obj" + $binDir = "$PSScriptRoot/bin" + $objDir = "$PSScriptRoot/obj" + foreach ($dir in $binDir,$objDir) + { + if (Test-Path $dir) + { + Remove-Item -Force -Recurse $dir + } + } } -dotnet build +Push-Location $PSScriptRoot +try +{ + dotnet build +} +finally +{ + Pop-Location +} From 558abc03f2ae2cca33be0f5ddb68566f0e0f6018 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 10:02:03 +1000 Subject: [PATCH 10/46] Rename pester test file to be more specific --- ...torServices.Tests.ps1 => EditorServices.Integration.Tests.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/Pester/{EditorServices.Tests.ps1 => EditorServices.Integration.Tests.ps1} (100%) diff --git a/test/Pester/EditorServices.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 similarity index 100% rename from test/Pester/EditorServices.Tests.ps1 rename to test/Pester/EditorServices.Integration.Tests.ps1 From 5c35561033a572ec05aba5b18e81620ea8a95a1c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 10:46:15 +1000 Subject: [PATCH 11/46] Kill process properly --- test/Pester/EditorServices.Integration.Tests.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index b62fb2183..dbb8052e2 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -8,7 +8,8 @@ Describe "Loading and running PowerShellEditorServices" { AfterAll { $pipe.Dispose() - $psesServer.Process.Close() + $psesServer.Process.Kill() + $psesServer.Process.Dispose() } It "Starts and responds to an initialization request" { From 81631a10653d6d147ce374b4ee77776182f7e09e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 10:53:28 +1000 Subject: [PATCH 12/46] Fix process kill --- test/Pester/EditorServices.Integration.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index dbb8052e2..c5bfa8c8a 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -8,8 +8,8 @@ Describe "Loading and running PowerShellEditorServices" { AfterAll { $pipe.Dispose() - $psesServer.Process.Kill() - $psesServer.Process.Dispose() + $psesServer.PsesProcess.Kill() + $psesServer.PsesProcess.Dispose() } It "Starts and responds to an initialization request" { From adcb52790dadaf6ad0a85113fa8564ba8394224f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 14:33:02 +1000 Subject: [PATCH 13/46] Add diagnostic line; --- scripts/azurePipelinesBuild.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/azurePipelinesBuild.ps1 b/scripts/azurePipelinesBuild.ps1 index ab31ed358..7ada7753c 100644 --- a/scripts/azurePipelinesBuild.ps1 +++ b/scripts/azurePipelinesBuild.ps1 @@ -15,3 +15,5 @@ Install-Module InvokeBuild -MaximumVersion 5.1.0 -Scope CurrentUser Install-Module PlatyPS -RequiredVersion 0.9.0 -Scope CurrentUser Invoke-Build -Configuration Release + +Write-Host 'Done' From b16429d75c84654bf84cd3517f19758af1d01fb2 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 23 Apr 2019 17:03:19 +1000 Subject: [PATCH 14/46] Attempt to exit --- scripts/azurePipelinesBuild.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/azurePipelinesBuild.ps1 b/scripts/azurePipelinesBuild.ps1 index 7ada7753c..d13291787 100644 --- a/scripts/azurePipelinesBuild.ps1 +++ b/scripts/azurePipelinesBuild.ps1 @@ -16,4 +16,4 @@ Install-Module PlatyPS -RequiredVersion 0.9.0 -Scope CurrentUser Invoke-Build -Configuration Release -Write-Host 'Done' +exit 0 From 0435cb66d1c120233ab321706edd30e55d8a96f4 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 24 Apr 2019 11:45:12 +1000 Subject: [PATCH 15/46] Add some documentation comments --- tools/PsesPsClient/LspPipe.cs | 45 ++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index 7cc3da998..cd01518f2 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -16,13 +16,21 @@ namespace PsesPsClient { + /// + /// A Language Server Protocol named pipe connection. + /// public class LspPipe : IDisposable { + /// + /// Create a new LSP pipe around a given named pipe. + /// + /// The name of the named pipe to use. + /// A new LspPipe instance around the given named pipe. public static LspPipe Create(string pipeName) { var pipeClient = new NamedPipeClientStream( - serverName: ".", pipeName: pipeName, + serverName: ".", direction: PipeDirection.InOut, options: PipeOptions.Asynchronous); @@ -45,6 +53,10 @@ public static LspPipe Create(string pipeName) private MessageStreamListener _listener; + /// + /// Create a new LSP pipe around a named pipe client stream. + /// + /// The named pipe client stream to use for the LSP pipe. public LspPipe(NamedPipeClientStream namedPipeClient) { _namedPipeClient = namedPipeClient; @@ -61,6 +73,9 @@ public LspPipe(NamedPipeClientStream namedPipeClient) _pipeEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); } + /// + /// Connect to the named pipe server. + /// public void Connect() { _namedPipeClient.Connect(timeout: 1000); @@ -73,6 +88,12 @@ public void Connect() _listener.Start(); } + /// + /// Write a request to the LSP pipe. + /// + /// The method of the request. + /// The parameters of the request. May be null. + /// A representation of the request sent. public LspRequest WriteRequest( string method, object parameters) @@ -96,16 +117,30 @@ public LspRequest WriteRequest( return new LspRequest(msg.Id, method, msgJson["params"]); } + /// + /// Get all the pending notifications from the server. + /// + /// Any pending notifications from the server. public IEnumerable GetNotifications() { return _listener.DrainNotifications(); } + /// + /// Get all the pending requests from the server. + /// + /// Any pending requests from the server. public IEnumerable GetRequests() { return _listener.DrainRequests(); } + /// + /// Get the next response from the server, if one is available within the given time. + /// + /// The next response from the server. + /// How long to wait for a response. + /// True if there is a next response, false if it timed out. public bool TryGetNextResponse(out LspResponse response, int millisTimeout) { return _listener.TryGetNextResponse(out response, millisTimeout); @@ -118,6 +153,10 @@ public void Dispose() } } + /// + /// A dedicated listener to run a thread for receiving pipe messages, + /// so the the pipe is not blocked. + /// public class MessageStreamListener : IDisposable { private readonly StreamReader _stream; @@ -134,6 +173,10 @@ public class MessageStreamListener : IDisposable private readonly CancellationTokenSource _cancellationSource; + /// + /// Create a listener around a stream. + /// + /// The stream to listen for messages on. public MessageStreamListener(StreamReader stream) { _stream = stream; From 7e9a76d53ab47b4bed5be7db3b96c3b56e08fe8a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 24 Apr 2019 11:58:09 +1000 Subject: [PATCH 16/46] Ensure pipe is closed during disposal --- tools/PsesPsClient/LspPipe.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index cd01518f2..2c51ea3ed 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -148,6 +148,8 @@ public bool TryGetNextResponse(out LspResponse response, int millisTimeout) public void Dispose() { + _namedPipeClient.Close(); + _writer.Dispose(); _listener.Dispose(); _namedPipeClient.Dispose(); } From 9b66b61080280c58a9e88fc6e95e827c3f0385cf Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 24 Apr 2019 12:12:20 +1000 Subject: [PATCH 17/46] Fix pipe disposal --- tools/PsesPsClient/LspPipe.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index 2c51ea3ed..16f9461dc 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -148,9 +148,9 @@ public bool TryGetNextResponse(out LspResponse response, int millisTimeout) public void Dispose() { - _namedPipeClient.Close(); _writer.Dispose(); _listener.Dispose(); + _namedPipeClient.Close(); _namedPipeClient.Dispose(); } } From d949558ed30cb82796762d9a41c681b13246308d Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 24 Apr 2019 17:55:37 +1000 Subject: [PATCH 18/46] Change cancellation method --- scripts/azurePipelinesBuild.ps1 | 2 -- tools/PsesPsClient/LspPipe.cs | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/azurePipelinesBuild.ps1 b/scripts/azurePipelinesBuild.ps1 index d13291787..ab31ed358 100644 --- a/scripts/azurePipelinesBuild.ps1 +++ b/scripts/azurePipelinesBuild.ps1 @@ -15,5 +15,3 @@ Install-Module InvokeBuild -MaximumVersion 5.1.0 -Scope CurrentUser Install-Module PlatyPS -RequiredVersion 0.9.0 -Scope CurrentUser Invoke-Build -Configuration Release - -exit 0 diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index 16f9461dc..af05cdc44 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -228,7 +228,8 @@ public void Dispose() private async Task RunListenLoop() { - while (!_cancellationSource.IsCancellationRequested) + CancellationToken cancellationToken = _cancellationSource.Token; + while (!cancellationToken.IsCancellationRequested) { LspMessage msg = await ReadMessage().ConfigureAwait(false); switch (msg) From b061b65dcb7f0c68a9ca5dc9ba373cad2f367518 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 25 Apr 2019 11:23:29 +1000 Subject: [PATCH 19/46] Try invoking pester in proc --- PowerShellEditorServices.build.ps1 | 4 +--- test/Pester/EditorServices.Integration.Tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index bcd0290b0..cd5d2b12d 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -377,9 +377,7 @@ task TestHost { } task TestPester Build,BuildPsesClientModule,EnsurePesterInstalled,{ - $pwshExe = (Get-Process -Id $PID).Path - $pesterTestDir = Resolve-Path "$PSScriptRoot/test/Pester/" - exec { & $pwshExe -Command "cd $pesterTestDir; Invoke-Pester" } + Invoke-Pester "$PSScriptRoot/test/Pester/" } task EnsurePesterInstalled { diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index c5bfa8c8a..ea4fc2a6e 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -25,7 +25,7 @@ Describe "Loading and running PowerShellEditorServices" { $pipe.TryGetNextResponse([ref]$response, 5000) | Should -BeTrue $response.Id | Should -BeExactly $request.Id $response.Result | Should -BeNull - # TODO: The server stays up waiting for the debug connection + # TODO: The server seems to stay up waiting for the debug connection # $psesServer.PsesProcess.HasExited | Should -BeTrue } } From 6b13e22ce2c8c58b2eea8c1ba6ab0fb25dff080b Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 25 Apr 2019 13:16:10 +1000 Subject: [PATCH 20/46] Add doc comments --- tools/PsesPsClient/LspPipe.cs | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index af05cdc44..d87af980c 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -146,6 +146,9 @@ public bool TryGetNextResponse(out LspResponse response, int millisTimeout) return _listener.TryGetNextResponse(out response, millisTimeout); } + /// + /// Dispose of the pipe. This will also close the pipe. + /// public void Dispose() { _writer.Dispose(); @@ -190,36 +193,62 @@ public MessageStreamListener(StreamReader stream) _cancellationSource = new CancellationTokenSource(); } + /// + /// Get all pending notifications. + /// public IEnumerable DrainNotifications() { return DrainQueue(_notificationQueue); } + /// + /// Get all pending requests. + /// public IEnumerable DrainRequests() { return DrainQueue(_requestQueue); } + /// + /// Get the next response if there is one, otherwise instantly return false. + /// + /// The first response in the response queue if any, otherwise null. + /// True if there was a response to get, false otherwise. public bool TryGetNextResponse(out LspResponse response) { return _responseBlockingOutput.TryTake(out response); } + /// + /// Get the next response within the given timeout. + /// + /// The first response in the queue, if any. + /// The maximum number of milliseconds to wait for a response. + /// True if there was a response to get, false otherwise. public bool TryGetNextResponse(out LspResponse response, int millisTimeout) { return _responseBlockingOutput.TryTake(out response, millisTimeout); } + /// + /// Start the pipe listener on its own thread. + /// public void Start() { Task.Run(() => RunListenLoop()); } + /// + /// End the pipe listener loop. + /// public void Stop() { _cancellationSource.Cancel(); } + /// + /// Stops and disposes the pipe listener. + /// public void Dispose() { Stop(); @@ -383,6 +412,9 @@ private static IEnumerable DrainQueue(ConcurrentQueue + /// Represents a Language Server Protocol message. + /// public abstract class LspMessage { protected LspMessage() @@ -390,6 +422,9 @@ protected LspMessage() } } + /// + /// A Language Server Protocol notifcation or event. + /// public class LspNotification : LspMessage { public LspNotification(string method, JToken parameters) @@ -398,11 +433,21 @@ public LspNotification(string method, JToken parameters) Params = parameters; } + /// + /// The notification method. + /// public string Method { get; } + /// + /// Any parameters for the notification. + /// public JToken Params { get; } } + /// + /// A Language Server Protocol request. + /// May be a client -> server or a server -> client request. + /// public class LspRequest : LspMessage { public LspRequest(string id, string method, JToken parameters) @@ -412,13 +457,25 @@ public LspRequest(string id, string method, JToken parameters) Params = parameters; } + /// + /// The ID of the request. Usually an integer. + /// public string Id { get; } + /// + /// The method of the request. + /// public string Method { get; } + /// + /// Any parameters of the request. + /// public JToken Params { get; } } + /// + /// A Language Server Protocol response message. + /// public abstract class LspResponse : LspMessage { protected LspResponse(string id) @@ -426,9 +483,15 @@ protected LspResponse(string id) Id = id; } + /// + /// The ID of the response. Will match the ID of the request triggering it. + /// public string Id { get; } } + /// + /// A successful Language Server Protocol response message. + /// public class LspSuccessfulResponse : LspResponse { public LspSuccessfulResponse(string id, JToken result) @@ -437,9 +500,15 @@ public LspSuccessfulResponse(string id, JToken result) Result = result; } + /// + /// The result field of the response. + /// public JToken Result { get; } } + /// + /// A Language Server Protocol error response message. + /// public class LspErrorResponse : LspResponse { public LspErrorResponse( @@ -454,13 +523,25 @@ public LspErrorResponse( Data = data; } + /// + /// The error code sent by the server, may not correspond to a known enum type. + /// public JsonRpcErrorCode Code { get; } + /// + /// The error message. + /// public string Message { get; } + /// + /// Extra error data. + /// public JToken Data { get; } } + /// + /// Error codes used by the Language Server Protocol. + /// public enum JsonRpcErrorCode : int { ParseError = -32700, From 29ccd298ff5b55e3c242840ec8b4a78a59433084 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 25 Apr 2019 13:18:44 +1000 Subject: [PATCH 21/46] Use better exceptions --- tools/PsesPsClient/LspPipe.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index d87af980c..9ffccc8de 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -370,7 +370,7 @@ private int GetContentLength() } } - throw new Exception("Buffer emptied before end of headers"); + throw new InvalidDataException("Buffer emptied before end of headers"); } private static int ParseContentLength(string headers) @@ -380,13 +380,13 @@ private static int ParseContentLength(string headers) int clIdx = headers.IndexOf(clHeaderPrefix); if (clIdx < 0) { - throw new Exception("No Content-Length header found"); + throw new InvalidDataException("No Content-Length header found"); } int endIdx = headers.IndexOf("\r\n", clIdx); if (endIdx < 0) { - throw new Exception("Header CRLF terminator not found"); + throw new InvalidDataException("Header CRLF terminator not found"); } int numStartIdx = clIdx + clHeaderPrefix.Length; From 2896933d5f7ad5078278b36ce4259d0314bc85a0 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 25 Apr 2019 13:22:45 +1000 Subject: [PATCH 22/46] Comply with some of Codacy's draconian views --- tools/PsesPsClient/LspPipe.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index 9ffccc8de..19ec9d492 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -377,13 +377,13 @@ private static int ParseContentLength(string headers) { const string clHeaderPrefix = "Content-Length: "; - int clIdx = headers.IndexOf(clHeaderPrefix); + int clIdx = headers.IndexOf(clHeaderPrefix, StringComparison.Ordinal); if (clIdx < 0) { throw new InvalidDataException("No Content-Length header found"); } - int endIdx = headers.IndexOf("\r\n", clIdx); + int endIdx = headers.IndexOf("\r\n", clIdx, StringComparison.Ordinal); if (endIdx < 0) { throw new InvalidDataException("Header CRLF terminator not found"); From 7b3ee709b83efae42af9c12206d856ff8ddc2969 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 26 Apr 2019 14:21:39 +1000 Subject: [PATCH 23/46] Publish Pester results in CI --- .vsts-ci/azure-pipelines-ci.yml | 5 +++++ .vsts-ci/templates/ci-general.yml | 5 +++++ PowerShellEditorServices.build.ps1 | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.vsts-ci/azure-pipelines-ci.yml b/.vsts-ci/azure-pipelines-ci.yml index e12ee8277..e2040f5e6 100644 --- a/.vsts-ci/azure-pipelines-ci.yml +++ b/.vsts-ci/azure-pipelines-ci.yml @@ -19,6 +19,11 @@ jobs: testRunner: VSTest testResultsFiles: '**/*.trx' condition: succeededOrFailed() + - task: PublishTestResults@2 + inputs: + testRunner: NUnit + testResultsFiles: '**/TestResults.xml' + condition: succeededOrFailed() - task: PublishBuildArtifacts@1 inputs: ArtifactName: PowerShellEditorServices diff --git a/.vsts-ci/templates/ci-general.yml b/.vsts-ci/templates/ci-general.yml index 328370aa3..6afbccf01 100644 --- a/.vsts-ci/templates/ci-general.yml +++ b/.vsts-ci/templates/ci-general.yml @@ -5,6 +5,11 @@ steps: testRunner: VSTest testResultsFiles: '**/*.trx' condition: succeededOrFailed() + - task: PublishTestResults@2 + inputs: + testRunner: NUnit + testResultsFiles: '**/TestResults.xml' + condition: suceededOrFailed() - task: PublishBuildArtifacts@1 inputs: ArtifactName: PowerShellEditorServices diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index cd5d2b12d..2e69bd632 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -377,7 +377,20 @@ task TestHost { } task TestPester Build,BuildPsesClientModule,EnsurePesterInstalled,{ - Invoke-Pester "$PSScriptRoot/test/Pester/" + $testParams = @{} + if ($env:TF_BUILD) + { + $testParams += @{ + OutputFormat = 'NUnitXml' + OutputFile = 'TestResults.xml' + } + } + $result = Invoke-Pester "$PSScriptRoot/test/Pester/" @testParams -PassThru + + if ($result.FailedCount -gt 0) + { + throw "$($result.FailedCount) tests failed." + } } task EnsurePesterInstalled { From 5a2b3d0c5f25463962d415945e7271c9a2c9be9e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 26 Apr 2019 14:24:03 +1000 Subject: [PATCH 24/46] Remove .vscode folder --- tools/PsesPsClient/.vscode/launch.json | 42 -------------------------- 1 file changed, 42 deletions(-) delete mode 100644 tools/PsesPsClient/.vscode/launch.json diff --git a/tools/PsesPsClient/.vscode/launch.json b/tools/PsesPsClient/.vscode/launch.json deleted file mode 100644 index 8b60d4fab..000000000 --- a/tools/PsesPsClient/.vscode/launch.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "WARNING01": "*********************************************************************************", - "WARNING02": "The C# extension was unable to automatically to decode projects in the current", - "WARNING03": "workspace to create a runnable lanch.json file. A template launch.json file has", - "WARNING04": "been created as a placeholder.", - "WARNING05": "", - "WARNING06": "If OmniSharp is currently unable to load your project, you can attempt to resolve", - "WARNING07": "this by restoring any missing project dependencies (example: run 'dotnet restore')", - "WARNING08": "and by fixing any reported errors from building the projects in your workspace.", - "WARNING09": "If this allows OmniSharp to now load your project then --", - "WARNING10": " * Delete this file", - "WARNING11": " * Open the Visual Studio Code command palette (View->Command Palette)", - "WARNING12": " * run the command: '.NET: Generate Assets for Build and Debug'.", - "WARNING13": "", - "WARNING14": "If your project requires a more complex launch configuration, you may wish to delete", - "WARNING15": "this configuration and pick a different template using the 'Add Configuration...'", - "WARNING16": "button at the bottom of this file.", - "WARNING17": "*********************************************************************************", - "preLaunchTask": "build", - "program": "${workspaceFolder}/bin/Debug//.dll", - "args": [], - "cwd": "${workspaceFolder}", - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" - } - ] -} \ No newline at end of file From 228b5eb0f306a5c0a696a3c1a919c7df99eb8e64 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 26 Apr 2019 14:51:25 +1000 Subject: [PATCH 25/46] Fix typo --- .vsts-ci/templates/ci-general.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vsts-ci/templates/ci-general.yml b/.vsts-ci/templates/ci-general.yml index 5ccf16be7..664d41d9e 100644 --- a/.vsts-ci/templates/ci-general.yml +++ b/.vsts-ci/templates/ci-general.yml @@ -18,7 +18,7 @@ steps: inputs: testRunner: NUnit testResultsFiles: '**/TestResults.xml' - condition: suceededOrFailed() + condition: succeededOrFailed() - task: PublishBuildArtifacts@1 inputs: ArtifactName: PowerShellEditorServices From 516cbb0ea1331f144297d9a4ad1a5c9b349df9ca Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 26 Apr 2019 14:59:42 +1000 Subject: [PATCH 26/46] Fix NRE in test --- test/Pester/EditorServices.Integration.Tests.ps1 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index ea4fc2a6e..eadeb2d2f 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -7,9 +7,16 @@ Describe "Loading and running PowerShellEditorServices" { } AfterAll { - $pipe.Dispose() - $psesServer.PsesProcess.Kill() - $psesServer.PsesProcess.Dispose() + try + { + $pipe.Dispose() + $psesServer.PsesProcess.Kill() + $psesServer.PsesProcess.Dispose() + } + catch + { + # Do nothing + } } It "Starts and responds to an initialization request" { From beeef028ff161add0b20cc2d9ef4a3fe19a8c5d9 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 26 Apr 2019 15:51:58 +1000 Subject: [PATCH 27/46] Improve client module building --- .../EditorServices.Integration.Tests.ps1 | 3 +- tools/PsesPsClient/LspPipe.cs | 7 +- tools/PsesPsClient/PsesPsClient.csproj | 2 +- tools/PsesPsClient/PsesPsClient.psd1 | 124 ++++++++++++++++++ tools/PsesPsClient/PsesPsClient.psm1 | 11 +- tools/PsesPsClient/build.ps1 | 28 +++- 6 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 tools/PsesPsClient/PsesPsClient.psd1 diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index eadeb2d2f..26f09a596 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -1,6 +1,7 @@ Describe "Loading and running PowerShellEditorServices" { BeforeAll { - Import-Module "$PSScriptRoot/../../tools/PsesPsClient" + Import-Module "$PSScriptRoot/../../module/PowerShellEditorServices" + Import-Module "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" $psesServer = Start-PsesServer $pipe = Connect-NamedPipe -PipeName $psesServer.SessionDetails.languageServicePipeName diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/LspPipe.cs index 19ec9d492..5da6e286f 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/LspPipe.cs @@ -1,4 +1,9 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.IO.Pipes; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; diff --git a/tools/PsesPsClient/PsesPsClient.csproj b/tools/PsesPsClient/PsesPsClient.csproj index ab2c9f6ac..6b30f989b 100644 --- a/tools/PsesPsClient/PsesPsClient.csproj +++ b/tools/PsesPsClient/PsesPsClient.csproj @@ -5,7 +5,7 @@ - + diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 new file mode 100644 index 000000000..6e6564ba2 --- /dev/null +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -0,0 +1,124 @@ +# +# Module manifest for module 'PsesPsClient' +# +# Generated by: Microsoft Corporation +# +# Generated on: 26/4/19 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'PsesPsClient.psm1' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +CompatiblePSEditions = 'Core', 'Desktop' + +# ID used to uniquely identify this module +GUID = 'ce491ff9-3eab-443c-b3a2-cc412ddeef65' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Unknown' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation' + +# Description of the functionality provided by this module +# Description = '' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '5.1' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# CLRVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +NestedModules = @('PsesPsClient.dll') + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = 'Start-PsesServer', 'Connect-NamedPipe', 'Send-LspInitializeRequest', + 'Send-LspShutdownRequest' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 7e2e6a53d..692b938db 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -1,9 +1,10 @@ -$script:PsesBundledModulesDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( - "$PSScriptRoot/../../module") - -Import-Module "$script:PsesBundledModulesDir/PowerShellEditorServices" +# +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# -Import-Module $PSScriptRoot/bin/Debug/netstandard2.0/PsesPsClient.dll +$script:PsesBundledModulesDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( + "$PSScriptRoot/../../../../module") class PsesStartupOptions { diff --git a/tools/PsesPsClient/build.ps1 b/tools/PsesPsClient/build.ps1 index 7558d64f7..53210daf8 100644 --- a/tools/PsesPsClient/build.ps1 +++ b/tools/PsesPsClient/build.ps1 @@ -1,13 +1,30 @@ +# +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + param( [switch] $Clean ) +$ErrorActionPreference = 'Stop' + +$script:OutDir = "$PSScriptRoot/out" +$script:OutModDir = "$script:OutDir/PsesPsClient" + +$script:ModuleComponents = @{ + "bin/Debug/netstandard2.0/publish/PsesPsClient.dll" = "PsesPsClient.dll" + "bin/Debug/netstandard2.0/publish/Newtonsoft.Json.dll" = "Newtonsoft.Json.dll" + "PsesPsClient.psm1" = "PsesPsClient.psm1" + "PsesPsClient.psd1" = "PsesPsClient.psd1" +} + if ($Clean) { $binDir = "$PSScriptRoot/bin" $objDir = "$PSScriptRoot/obj" - foreach ($dir in $binDir,$objDir) + foreach ($dir in $binDir,$objDir,$script:OutDir) { if (Test-Path $dir) { @@ -19,7 +36,14 @@ if ($Clean) Push-Location $PSScriptRoot try { - dotnet build + dotnet publish + + New-Item -Path $script:OutModDir -ItemType Directory + foreach ($key in $script:ModuleComponents.get_Keys()) + { + $val = $script:ModuleComponents[$key] + Copy-Item -Path "$PSScriptRoot/$key" -Destination "$script:OutModDir/$val" + } } finally { From dae751140bc47294b0f79ccea7574219288f43c4 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 26 Apr 2019 16:09:41 +1000 Subject: [PATCH 28/46] Pass through dotnet location --- PowerShellEditorServices.build.ps1 | 4 ++-- tools/PsesPsClient/build.ps1 | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 728f617e7..6c20aaa55 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -329,8 +329,8 @@ task Build { exec { & $script:dotnetExe build -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj $script:TargetFrameworksParam } } -task BuildPsesClientModule { - & $PSScriptRoot/tools/PsesPsClient/build.ps1 -Clean +task BuildPsesClientModule SetupDotNet,{ + & $PSScriptRoot/tools/PsesPsClient/build.ps1 -Clean -DotnetExe $script:dotnetExe } function DotNetTestFilter { diff --git a/tools/PsesPsClient/build.ps1 b/tools/PsesPsClient/build.ps1 index 53210daf8..06fb8b4b0 100644 --- a/tools/PsesPsClient/build.ps1 +++ b/tools/PsesPsClient/build.ps1 @@ -4,6 +4,10 @@ # param( + [Parameter()] + [string] + $DotnetExe = 'dotnet', + [switch] $Clean ) @@ -36,7 +40,7 @@ if ($Clean) Push-Location $PSScriptRoot try { - dotnet publish + & $DotnetExe publish --framework 'netstandard2.0' New-Item -Path $script:OutModDir -ItemType Directory foreach ($key in $script:ModuleComponents.get_Keys()) From f8654d6492e3a7658d86e5a59b2d2dabd3c1749f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 26 Apr 2019 16:20:51 +1000 Subject: [PATCH 29/46] Ensure new enough Pester is installed --- PowerShellEditorServices.build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 6c20aaa55..00c4317cb 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -18,11 +18,11 @@ param( #Requires -Modules @{ModuleName="InvokeBuild";ModuleVersion="3.2.1"} -$script:IsCIBuild = $env:TF_BUILD -ne $null $script:IsUnix = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq "Core" -and !$IsWindows $script:TargetPlatform = "netstandard2.0" $script:TargetFrameworksParam = "/p:TargetFrameworks=`"$script:TargetPlatform`"" $script:RequiredSdkVersion = (Get-Content (Join-Path $PSScriptRoot 'global.json') | ConvertFrom-Json).sdk.version +$script:MinimumPesterVersion = '4.7' $script:NugetApiUriBase = 'https://www.nuget.org/api/v2/package' $script:ModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices/bin/" $script:VSCodeModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices.VSCode/bin/" @@ -394,7 +394,7 @@ task TestPester Build,BuildPsesClientModule,EnsurePesterInstalled,{ } task EnsurePesterInstalled { - if (Get-Command Invoke-Pester -ErrorAction SilentlyContinue) + if (Get-Module Pester -ListAvailable | Where-Object { $_.Version -ge $script:MinimumPesterVersion}) { return } From 14b88843e16991b1e91c0a51219940badfd0536c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 26 Apr 2019 16:30:05 +1000 Subject: [PATCH 30/46] Add -Force flag --- PowerShellEditorServices.build.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 00c4317cb..4857165e7 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -399,7 +399,8 @@ task EnsurePesterInstalled { return } - Install-Module -Scope CurrentUser Pester + Write-Warning "Required Pester version not found, installing Pester to current user scope" + Install-Module -Scope CurrentUser Pester -Force } task LayoutModule -After Build { From 4069a404340b555eee7a84effc40624150c65055 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 30 Apr 2019 10:48:19 +1000 Subject: [PATCH 31/46] Update tools/PsesPsClient/PsesPsClient.psd1 Co-Authored-By: rjmholt --- tools/PsesPsClient/PsesPsClient.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 6e6564ba2..9763583eb 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -24,7 +24,7 @@ GUID = 'ce491ff9-3eab-443c-b3a2-cc412ddeef65' Author = 'Microsoft Corporation' # Company or vendor of this module -CompanyName = 'Unknown' +CompanyName = 'Microsoft Corporation' # Copyright statement for this module Copyright = '(c) Microsoft Corporation' From a6e2e992f90c8d8b65df859f8af2b59489955fb0 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 30 Apr 2019 13:14:35 +1000 Subject: [PATCH 32/46] Rework module, address @TylerLeonhardt's feedback --- PowerShellEditorServices.build.ps1 | 10 +-- .../EditorServices.Integration.Tests.ps1 | 18 +++-- tools/PsesPsClient/{LspPipe.cs => Client.cs} | 50 +++++++++----- tools/PsesPsClient/PsesPsClient.psd1 | 10 ++- tools/PsesPsClient/PsesPsClient.psm1 | 68 +++++++++++++++---- 5 files changed, 108 insertions(+), 48 deletions(-) rename tools/PsesPsClient/{LspPipe.cs => Client.cs} (91%) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 4857165e7..7e5dc976d 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -330,6 +330,7 @@ task Build { } task BuildPsesClientModule SetupDotNet,{ + Write-Verbose 'Building PsesPsClient testing module' & $PSScriptRoot/tools/PsesPsClient/build.ps1 -Clean -DotnetExe $script:dotnetExe } @@ -393,14 +394,9 @@ task TestPester Build,BuildPsesClientModule,EnsurePesterInstalled,{ } } -task EnsurePesterInstalled { - if (Get-Module Pester -ListAvailable | Where-Object { $_.Version -ge $script:MinimumPesterVersion}) - { - return - } - +task EnsurePesterInstalled -If (-not (Get-Module Pester -ListAvailable | Where-Object Version -GE $script:MinimumPesterVersion)) { Write-Warning "Required Pester version not found, installing Pester to current user scope" - Install-Module -Scope CurrentUser Pester -Force + Install-Module -Scope CurrentUser Pester -Force -SkipPublisherCheck } task LayoutModule -After Build { diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 26f09a596..1a024dac1 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -1,16 +1,16 @@ Describe "Loading and running PowerShellEditorServices" { BeforeAll { - Import-Module "$PSScriptRoot/../../module/PowerShellEditorServices" - Import-Module "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" + Import-Module -Force "$PSScriptRoot/../../module/PowerShellEditorServices" + Import-Module -Force "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" $psesServer = Start-PsesServer - $pipe = Connect-NamedPipe -PipeName $psesServer.SessionDetails.languageServicePipeName + $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } AfterAll { try { - $pipe.Dispose() + $client.Dispose() $psesServer.PsesProcess.Kill() $psesServer.PsesProcess.Dispose() } @@ -21,16 +21,14 @@ Describe "Loading and running PowerShellEditorServices" { } It "Starts and responds to an initialization request" { - $request = Send-LspInitializeRequest -Pipe $pipe - $response = $null - $pipe.TryGetNextResponse([ref]$response, 5000) | Should -BeTrue + $request = Send-LspInitializeRequest -Client $client + $response = Get-LspResponse -Client $client -Id $request.Id $response.Id | Should -BeExactly $request.Id } It "Shuts down the process properly" { - $request = Send-LspShutdownRequest -Pipe $pipe - $response = $null - $pipe.TryGetNextResponse([ref]$response, 5000) | Should -BeTrue + $request = Send-LspShutdownRequest -Client $client + $response = Get-LspResponse -Client $client -Id $request.Id $response.Id | Should -BeExactly $request.Id $response.Result | Should -BeNull # TODO: The server seems to stay up waiting for the debug connection diff --git a/tools/PsesPsClient/LspPipe.cs b/tools/PsesPsClient/Client.cs similarity index 91% rename from tools/PsesPsClient/LspPipe.cs rename to tools/PsesPsClient/Client.cs index 5da6e286f..9953cc5cb 100644 --- a/tools/PsesPsClient/LspPipe.cs +++ b/tools/PsesPsClient/Client.cs @@ -24,14 +24,14 @@ namespace PsesPsClient /// /// A Language Server Protocol named pipe connection. /// - public class LspPipe : IDisposable + public class PsesLspClient : IDisposable { /// /// Create a new LSP pipe around a given named pipe. /// /// The name of the named pipe to use. /// A new LspPipe instance around the given named pipe. - public static LspPipe Create(string pipeName) + public static PsesLspClient Create(string pipeName) { var pipeClient = new NamedPipeClientStream( pipeName: pipeName, @@ -39,7 +39,7 @@ public static LspPipe Create(string pipeName) direction: PipeDirection.InOut, options: PipeOptions.Asynchronous); - return new LspPipe(pipeClient); + return new PsesLspClient(pipeClient); } private readonly NamedPipeClientStream _namedPipeClient; @@ -62,7 +62,7 @@ public static LspPipe Create(string pipeName) /// Create a new LSP pipe around a named pipe client stream. /// /// The named pipe client stream to use for the LSP pipe. - public LspPipe(NamedPipeClientStream namedPipeClient) + public PsesLspClient(NamedPipeClientStream namedPipeClient) { _namedPipeClient = namedPipeClient; @@ -73,6 +73,7 @@ public LspPipe(NamedPipeClientStream namedPipeClient) _jsonSerializer = JsonSerializer.Create(_jsonSettings); + // Reuse the PSES JSON RPC serializer _jsonRpcSerializer = new JsonRpcMessageSerializer(); _pipeEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); @@ -146,9 +147,9 @@ public IEnumerable GetRequests() /// The next response from the server. /// How long to wait for a response. /// True if there is a next response, false if it timed out. - public bool TryGetNextResponse(out LspResponse response, int millisTimeout) + public bool TryGetResponse(string id, out LspResponse response, int millisTimeout) { - return _listener.TryGetNextResponse(out response, millisTimeout); + return _listener.TryGetResponse(id, out response, millisTimeout); } /// @@ -177,12 +178,14 @@ public class MessageStreamListener : IDisposable private readonly ConcurrentQueue _notificationQueue; - private readonly BlockingCollection _responseBlockingOutput; - - private char[] _readerBuffer; + private readonly ConcurrentDictionary _responses; private readonly CancellationTokenSource _cancellationSource; + private readonly BlockingCollection _responseReceivedChannel; + + private char[] _readerBuffer; + /// /// Create a listener around a stream. /// @@ -194,8 +197,9 @@ public MessageStreamListener(StreamReader stream) _headerBuffer = new StringBuilder(128); _notificationQueue = new ConcurrentQueue(); _requestQueue = new ConcurrentQueue(); - _responseBlockingOutput = new BlockingCollection(); + _responses = new ConcurrentDictionary(); _cancellationSource = new CancellationTokenSource(); + _responseReceivedChannel = new BlockingCollection(); } /// @@ -219,9 +223,10 @@ public IEnumerable DrainRequests() /// /// The first response in the response queue if any, otherwise null. /// True if there was a response to get, false otherwise. - public bool TryGetNextResponse(out LspResponse response) + public bool TryGetResponse(string id, out LspResponse response) { - return _responseBlockingOutput.TryTake(out response); + _responseReceivedChannel.TryTake(out bool _, millisecondsTimeout: 0); + return _responses.TryRemove(id, out response); } /// @@ -230,9 +235,20 @@ public bool TryGetNextResponse(out LspResponse response) /// The first response in the queue, if any. /// The maximum number of milliseconds to wait for a response. /// True if there was a response to get, false otherwise. - public bool TryGetNextResponse(out LspResponse response, int millisTimeout) + public bool TryGetResponse(string id, out LspResponse response, int millisTimeout) { - return _responseBlockingOutput.TryTake(out response, millisTimeout); + if (_responses.TryRemove(id, out response)) + { + return true; + } + + if (_responseReceivedChannel.TryTake(out bool _, millisTimeout)) + { + return _responses.TryRemove(id, out response); + } + + response = null; + return false; } /// @@ -265,7 +281,8 @@ private async Task RunListenLoop() CancellationToken cancellationToken = _cancellationSource.Token; while (!cancellationToken.IsCancellationRequested) { - LspMessage msg = await ReadMessage().ConfigureAwait(false); + LspMessage msg; + msg = await ReadMessage().ConfigureAwait(false); switch (msg) { case LspNotification notification: @@ -273,7 +290,8 @@ private async Task RunListenLoop() continue; case LspResponse response: - _responseBlockingOutput.Add(response); + _responses[response.Id] = response; + _responseReceivedChannel.Add(true); continue; case LspRequest request: diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 9763583eb..c102d30c8 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -69,8 +69,14 @@ PowerShellVersion = '5.1' NestedModules = @('PsesPsClient.dll') # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Start-PsesServer', 'Connect-NamedPipe', 'Send-LspInitializeRequest', - 'Send-LspShutdownRequest' +FunctionsToExport = @( + 'Start-PsesServer', + 'Connect-PsesServer', + 'Send-LspRequest', + 'Send-LspInitializeRequest', + 'Send-LspShutdownRequest', + 'Get-LspResponse' +) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = '*' diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 692b938db..4e8a2c158 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -23,9 +23,7 @@ class PsesStartupOptions class PsesServerInfo { [pscustomobject]$SessionDetails - [System.Diagnostics.Process]$PsesProcess - [PsesStartupOptions]$StartupOptions } @@ -161,9 +159,9 @@ function Start-PsesServer } } -function Connect-NamedPipe +function Connect-PsesServer { - [OutputType([PsesPsClient.LspPipe])] + [OutputType([PsesPsClient.PsesLspClient])] param( [Parameter(Mandatory)] [string] @@ -176,9 +174,9 @@ function Connect-NamedPipe $PipeName = $PipeName.Substring($psesIdx) } - $pipe = [PsesPsClient.LspPipe]::Create($PipeName) - $pipe.Connect() - return $pipe + $client = [PsesPsClient.PsesLspClient]::Create($PipeName) + $client.Connect() + return $client } function Send-LspInitializeRequest @@ -186,8 +184,8 @@ function Send-LspInitializeRequest [OutputType([PsesPsClient.LspRequest])] param( [Parameter(Position = 0, Mandatory)] - [PsesPsClient.LspPipe] - $Pipe, + [PsesPsClient.PsesLspClient] + $Client, [Parameter()] [int] @@ -225,7 +223,7 @@ function Send-LspInitializeRequest $parameters.RootPath = $RootPath } - return $Pipe.WriteRequest('initialize', $parameters) + return Send-LspRequest -Client $Client -Method 'initialize' -Parameters $parameters } function Send-LspShutdownRequest @@ -233,11 +231,55 @@ function Send-LspShutdownRequest [OutputType([PsesPsClient.LspRequest])] param( [Parameter(Position = 0, Mandatory)] - [PsesPsClient.LspPipe] - $Pipe + [PsesPsClient.PsesLspClient] + $Client + ) + + return Send-LspRequest -Client $Client -Method 'shutdown' +} + +function Send-LspRequest +{ + [OutputType([PsesPsClient.LspRequest])] + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.PsesLspClient] + $Client, + + [Parameter(Position = 1, Mandatory)] + [string] + $Method, + + [Parameter(Position = 2)] + $Parameters = $null + ) + + return $Client.WriteRequest($Method, $Parameters) +} + +function Get-LspResponse +{ + [OutputType([PsesPsClient.LspResponse])] + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.PsesLspClient] + $Client, + + [Parameter(Position = 1, Mandatory)] + [string] + $Id, + + [Parameter()] + [int] + $WaitMillis = 5000 ) - $Pipe.WriteRequest('shutdown', $null) + $lspResponse = $null + + if ($Client.TryGetResponse($Id, [ref]$lspResponse, $WaitMillis)) + { + return $lspResponse + } } function Unsplat From e8529dd28d2f3d06b025c33b43d527857050306c Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 3 May 2019 16:45:40 +1000 Subject: [PATCH 33/46] Update PowerShellEditorServices.build.ps1 Co-Authored-By: rjmholt --- PowerShellEditorServices.build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 7e5dc976d..163f93ead 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -394,7 +394,7 @@ task TestPester Build,BuildPsesClientModule,EnsurePesterInstalled,{ } } -task EnsurePesterInstalled -If (-not (Get-Module Pester -ListAvailable | Where-Object Version -GE $script:MinimumPesterVersion)) { +task EnsurePesterInstalled -If (-not (Get-Module Pester -ListAvailable | Where-Object Version -ge $script:MinimumPesterVersion)) { Write-Warning "Required Pester version not found, installing Pester to current user scope" Install-Module -Scope CurrentUser Pester -Force -SkipPublisherCheck } From b0d704c549ac8b50950c1cfd41ff27f69e9c3036 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 9 May 2019 13:15:35 -0700 Subject: [PATCH 34/46] Update tools/PsesPsClient/Client.cs --- tools/PsesPsClient/Client.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/PsesPsClient/Client.cs b/tools/PsesPsClient/Client.cs index 9953cc5cb..bae8c7247 100644 --- a/tools/PsesPsClient/Client.cs +++ b/tools/PsesPsClient/Client.cs @@ -378,7 +378,6 @@ private int GetContentLength() continue; } - // This is the end, my only friend, the end if (endHeaderState == 3) { return ParseContentLength(_headerBuffer.ToString()); From 1b90683199b89daf2eb778a5744f3bb23e7d1a3b Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 14 May 2019 10:09:37 -0700 Subject: [PATCH 35/46] Add server output to integration test --- PowerShellEditorServices.build.ps1 | 2 +- .../EditorServices.Integration.Tests.ps1 | 2 +- tools/PsesPsClient/PsesPsClient.psm1 | 8 ++++++-- tools/PsesPsClient/build.ps1 | 18 ++++++------------ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 163f93ead..d28c9ffa8 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -331,7 +331,7 @@ task Build { task BuildPsesClientModule SetupDotNet,{ Write-Verbose 'Building PsesPsClient testing module' - & $PSScriptRoot/tools/PsesPsClient/build.ps1 -Clean -DotnetExe $script:dotnetExe + & $PSScriptRoot/tools/PsesPsClient/build.ps1 -DotnetExe $script:dotnetExe } function DotNetTestFilter { diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 1a024dac1..4c2db45a7 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -3,7 +3,7 @@ Describe "Loading and running PowerShellEditorServices" { Import-Module -Force "$PSScriptRoot/../../module/PowerShellEditorServices" Import-Module -Force "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" - $psesServer = Start-PsesServer + $psesServer = Start-PsesServer -NoNewWindow $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 4e8a2c158..f17a2e538 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -84,7 +84,11 @@ function Start-PsesServer [Parameter()] [switch] - $EnableConsoleRepl + $EnableConsoleRepl, + + [Parameter()] + [switch] + $NoNewWindow ) $EditorServicesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($EditorServicesPath) @@ -130,7 +134,7 @@ function Start-PsesServer return } - $serverProcess = Start-Process -PassThru -FilePath $pwshPath -ArgumentList @( + $serverProcess = Start-Process -PassThru -FilePath $pwshPath -NoNewWindow:$NoNewWindow -ArgumentList @( '-NoLogo', '-NoProfile', '-NoExit', diff --git a/tools/PsesPsClient/build.ps1 b/tools/PsesPsClient/build.ps1 index 06fb8b4b0..a9da9a778 100644 --- a/tools/PsesPsClient/build.ps1 +++ b/tools/PsesPsClient/build.ps1 @@ -6,10 +6,7 @@ param( [Parameter()] [string] - $DotnetExe = 'dotnet', - - [switch] - $Clean + $DotnetExe = 'dotnet' ) $ErrorActionPreference = 'Stop' @@ -24,16 +21,13 @@ $script:ModuleComponents = @{ "PsesPsClient.psd1" = "PsesPsClient.psd1" } -if ($Clean) +$binDir = "$PSScriptRoot/bin" +$objDir = "$PSScriptRoot/obj" +foreach ($dir in $binDir,$objDir,$script:OutDir) { - $binDir = "$PSScriptRoot/bin" - $objDir = "$PSScriptRoot/obj" - foreach ($dir in $binDir,$objDir,$script:OutDir) + if (Test-Path $dir) { - if (Test-Path $dir) - { - Remove-Item -Force -Recurse $dir - } + Remove-Item -Force -Recurse $dir } } From c0bcefbfc34019fad0e645532f6594afa49dbb9c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 14 May 2019 10:33:27 -0700 Subject: [PATCH 36/46] Try stderr recording --- .../EditorServices.Integration.Tests.ps1 | 13 ++++++++++- tools/PsesPsClient/PsesPsClient.psm1 | 23 +++++++++++-------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 4c2db45a7..00e39c801 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -3,11 +3,22 @@ Describe "Loading and running PowerShellEditorServices" { Import-Module -Force "$PSScriptRoot/../../module/PowerShellEditorServices" Import-Module -Force "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" - $psesServer = Start-PsesServer -NoNewWindow + $stderrFile = [System.IO.Path]::GetTempFileName() + + $psesServer = Start-PsesServer -StderrFile $stderrFile $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } AfterAll { + if (Test-Path $stderrFile) + { + $errorMessages = Get-Content -Raw $stderrFile + if ($errorMessages) + { + Write-Error $errorMessages + } + } + try { $client.Dispose() diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index f17a2e538..8b7a30d38 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -87,8 +87,8 @@ function Start-PsesServer $EnableConsoleRepl, [Parameter()] - [switch] - $NoNewWindow + [string] + $StderrFile ) $EditorServicesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($EditorServicesPath) @@ -134,13 +134,18 @@ function Start-PsesServer return } - $serverProcess = Start-Process -PassThru -FilePath $pwshPath -NoNewWindow:$NoNewWindow -ArgumentList @( - '-NoLogo', - '-NoProfile', - '-NoExit', - '-Command', - $startPsesCommand - ) + $startProcParams = @{ + PassThru = $true + FilePath = $pwshPath + ArgumentList = '-NoLogo','-NoProfile','-NoExit','-Command',$startPsesCommand + } + + if ($StderrFile) + { + $startProcParams.RedirectStandardError = $StderrFile + } + + $serverProcess = Start-Process @startProcParams $sessionPath = $editorServicesOptions.SessionDetailsPath From d9b3a0b6f01bd94763b9f51734b9254ee10517f2 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 14 May 2019 10:56:47 -0700 Subject: [PATCH 37/46] Try procinfo --- .../EditorServices.Integration.Tests.ps1 | 13 ++++--- tools/PsesPsClient/PsesPsClient.psm1 | 35 +++++++++++++------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 00e39c801..c0b40ed19 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -5,18 +5,17 @@ Describe "Loading and running PowerShellEditorServices" { $stderrFile = [System.IO.Path]::GetTempFileName() - $psesServer = Start-PsesServer -StderrFile $stderrFile + $psesServer = Start-PsesServer -RetainOutput $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } AfterAll { - if (Test-Path $stderrFile) + $errs = $psesServer.StandardError.ReadToEnd() + + if ($errs) { - $errorMessages = Get-Content -Raw $stderrFile - if ($errorMessages) - { - Write-Error $errorMessages - } + Write-Host 'ERRORS with EditorServices server:' + Write-Error $errs } try diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 8b7a30d38..f55577fdb 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -87,8 +87,8 @@ function Start-PsesServer $EnableConsoleRepl, [Parameter()] - [string] - $StderrFile + [switch] + $RetainOutput ) $EditorServicesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($EditorServicesPath) @@ -134,19 +134,32 @@ function Start-PsesServer return } - $startProcParams = @{ - PassThru = $true - FilePath = $pwshPath - ArgumentList = '-NoLogo','-NoProfile','-NoExit','-Command',$startPsesCommand - } + $startArgs = @( + '-NoLogo', + '-NoProfile', + '-NoExit', + '-Command', + $startPsesCommand + ) - if ($StderrFile) + if ($RetainOutput) + { + $serverProcess = [System.Diagnostics.Process]@{ + StartInfo = [System.Diagnostics.ProcessStartInfo]@{ + FileName = $pwshPath + RedirectStandardOutput = $true + RedirectStandardError = $true + UseShellExecute = $false + Arguments = ($startArgs -join ' ').Replace("'", "'''") + } + } + $serverProcess.Start() + } + else { - $startProcParams.RedirectStandardError = $StderrFile + $serverProcess = Start-Process -PassThru -FilePath $pwshPath -ArgumentList $startArgs } - $serverProcess = Start-Process @startProcParams - $sessionPath = $editorServicesOptions.SessionDetailsPath $i = 0 From c48b586c8a82db8c6191d0e6ecad04e6aed40ed5 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 14 May 2019 11:03:45 -0700 Subject: [PATCH 38/46] Don't use stream when it doesn't exist --- test/Pester/EditorServices.Integration.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index c0b40ed19..59f0badbb 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -10,7 +10,7 @@ Describe "Loading and running PowerShellEditorServices" { } AfterAll { - $errs = $psesServer.StandardError.ReadToEnd() + $errs = if ($psesServer.StandardError) { $psesServer.StandardError.ReadToEnd() } if ($errs) { From e2e3c9b8f35ba19b45fc9f2645b1de986f9b4bf2 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 14 May 2019 11:16:31 -0700 Subject: [PATCH 39/46] Try again with error file --- .../EditorServices.Integration.Tests.ps1 | 4 +-- tools/PsesPsClient/PsesPsClient.psm1 | 27 ++++++++----------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 59f0badbb..5001ba621 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -5,12 +5,12 @@ Describe "Loading and running PowerShellEditorServices" { $stderrFile = [System.IO.Path]::GetTempFileName() - $psesServer = Start-PsesServer -RetainOutput + $psesServer = Start-PsesServer -ErrorFile $stderrFile $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } AfterAll { - $errs = if ($psesServer.StandardError) { $psesServer.StandardError.ReadToEnd() } + $errs = if (Test-Path $stderrFile) { Get-Content -Raw $stderrFile } if ($errs) { diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index f55577fdb..cf87c4842 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -87,8 +87,8 @@ function Start-PsesServer $EnableConsoleRepl, [Parameter()] - [switch] - $RetainOutput + [string] + $ErrorFile ) $EditorServicesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($EditorServicesPath) @@ -142,24 +142,19 @@ function Start-PsesServer $startPsesCommand ) - if ($RetainOutput) - { - $serverProcess = [System.Diagnostics.Process]@{ - StartInfo = [System.Diagnostics.ProcessStartInfo]@{ - FileName = $pwshPath - RedirectStandardOutput = $true - RedirectStandardError = $true - UseShellExecute = $false - Arguments = ($startArgs -join ' ').Replace("'", "'''") - } - } - $serverProcess.Start() + $startProcParams = @{ + PassThru = $true + FilePath = $pwshPath + ArgumentList = $startArgs } - else + + if ($ErrorFile) { - $serverProcess = Start-Process -PassThru -FilePath $pwshPath -ArgumentList $startArgs + $startProcParams.RedirectStandardError = $ErrorFile } + $serverProcess = Start-Process @startProcParams + $sessionPath = $editorServicesOptions.SessionDetailsPath $i = 0 From a7e599c1857ee8996b03dc940c53d1092e3afd21 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 14 May 2019 11:44:18 -0700 Subject: [PATCH 40/46] Use log parser to find errors --- .../EditorServices.Integration.Tests.ps1 | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 5001ba621..113455978 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -2,22 +2,17 @@ Describe "Loading and running PowerShellEditorServices" { BeforeAll { Import-Module -Force "$PSScriptRoot/../../module/PowerShellEditorServices" Import-Module -Force "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" + Import-Module -Force "$PSScriptRoot/../../tools/PsesLogAnalyzer" $stderrFile = [System.IO.Path]::GetTempFileName() - $psesServer = Start-PsesServer -ErrorFile $stderrFile + $logPath = Join-Path ([System.IO.Path]::GetTempPath()) 'PSES_IntegrationTest.log' + + $psesServer = Start-PsesServer -ErrorFile $stderrFile -LogPath $logPath $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } AfterAll { - $errs = if (Test-Path $stderrFile) { Get-Content -Raw $stderrFile } - - if ($errs) - { - Write-Host 'ERRORS with EditorServices server:' - Write-Error $errs - } - try { $client.Dispose() @@ -28,6 +23,15 @@ Describe "Loading and running PowerShellEditorServices" { { # Do nothing } + + $errorLogs = Parse-PsesLog $logPath | + Where-Object LogLevel -eq Error + + if ($errorLogs) + { + $errorLogs | ForEach-Object { Write-Error $_.Message.Data } + throw "Error found in logs post execution" + } } It "Starts and responds to an initialization request" { From 47cdb904708e175401c2c7b53336ba91050b0b85 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 14 May 2019 14:02:46 -0700 Subject: [PATCH 41/46] Add test exception for pwsh pipe close error --- .../EditorServices.Integration.Tests.ps1 | 53 ++++++++++++++----- tools/PsesPsClient/PsesPsClient.psm1 | 2 + 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 113455978..f42f988ae 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -1,14 +1,43 @@ + +$script:ExceptionRegex = [regex]::new('\s*Exception: (.*)$', 'Compiled,Multiline,IgnoreCase') +function ReportLogErrors +{ + param( + [Parameter()][string]$LogPath, + + [Parameter()][ref]<#[int]#>$FromIndex = 0, + + [Parameter()][string[]]$IgnoreException = @() + ) + + $logEntries = Parse-PsesLog $LogPath | + Where-Object Index -ge $FromIndex.Value + + # Update the index to the latest in the log + $FromIndex.Value = ($FromIndex.Value,$errorLogs.Index | Measure-Object -Maximum).Maximum + + $errorLogs = $logEntries | + Where-Object LogLevel -eq Error | + Where-Object { + $match = $script:ExceptionRegex.Match($_.Message.Data) + + (-not $match) -or ($match.Groups[1].Value.Trim() -notin $IgnoreException) + } + + if ($errorLogs) + { + $errorLogs | ForEach-Object { Write-Error $_.Message.Data } + } +} + Describe "Loading and running PowerShellEditorServices" { BeforeAll { Import-Module -Force "$PSScriptRoot/../../module/PowerShellEditorServices" Import-Module -Force "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" Import-Module -Force "$PSScriptRoot/../../tools/PsesLogAnalyzer" - $stderrFile = [System.IO.Path]::GetTempFileName() - - $logPath = Join-Path ([System.IO.Path]::GetTempPath()) 'PSES_IntegrationTest.log' - - $psesServer = Start-PsesServer -ErrorFile $stderrFile -LogPath $logPath + $logIdx = 0 + $psesServer = Start-PsesServer $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } @@ -24,20 +53,16 @@ Describe "Loading and running PowerShellEditorServices" { # Do nothing } - $errorLogs = Parse-PsesLog $logPath | - Where-Object LogLevel -eq Error - - if ($errorLogs) - { - $errorLogs | ForEach-Object { Write-Error $_.Message.Data } - throw "Error found in logs post execution" - } + # TODO: We shouldn't need to skip this error + ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) -IgnoreException 'EndOfStreamException' } It "Starts and responds to an initialization request" { $request = Send-LspInitializeRequest -Client $client $response = Get-LspResponse -Client $client -Id $request.Id $response.Id | Should -BeExactly $request.Id + + ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } It "Shuts down the process properly" { @@ -47,5 +72,7 @@ Describe "Loading and running PowerShellEditorServices" { $response.Result | Should -BeNull # TODO: The server seems to stay up waiting for the debug connection # $psesServer.PsesProcess.HasExited | Should -BeTrue + + ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } } diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index cf87c4842..a66e5c7d1 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -25,6 +25,7 @@ class PsesServerInfo [pscustomobject]$SessionDetails [System.Diagnostics.Process]$PsesProcess [PsesStartupOptions]$StartupOptions + [string]$LogPath } function Start-PsesServer @@ -173,6 +174,7 @@ function Start-PsesServer PsesProcess = $serverProcess SessionDetails = Get-Content -Raw $editorServicesOptions.SessionDetailsPath | ConvertFrom-Json StartupOptions = $editorServicesOptions + LogPath = $LogPath } } From d5a67c2616f25abdda7f39aa14e0ddca779b06f6 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 14 May 2019 14:12:50 -0700 Subject: [PATCH 42/46] Improve error message --- test/Pester/EditorServices.Integration.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index f42f988ae..06a92615a 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -26,7 +26,7 @@ function ReportLogErrors if ($errorLogs) { - $errorLogs | ForEach-Object { Write-Error $_.Message.Data } + $errorLogs | ForEach-Object { Write-Error "ERROR from PSES log: $($_.Message.Data)" } } } From dc2506e9cd6213cc1c94ae7266621c8956d0e02e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 15 May 2019 14:02:53 -0700 Subject: [PATCH 43/46] Test without skipping crash log exception --- test/Pester/EditorServices.Integration.Tests.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 06a92615a..227b1519a 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -53,8 +53,9 @@ Describe "Loading and running PowerShellEditorServices" { # Do nothing } - # TODO: We shouldn't need to skip this error - ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) -IgnoreException 'EndOfStreamException' + # TODO: We shouldn't need to skip this error. + # It's not clear why we get it but it only occurs on Windows + ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) #-IgnoreException 'EndOfStreamException' } It "Starts and responds to an initialization request" { From 135d47db9992e782efab12aa2dc61b4b696a206f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 15 May 2019 14:08:20 -0700 Subject: [PATCH 44/46] Try ending server stream first --- test/Pester/EditorServices.Integration.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 227b1519a..cf0c05869 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -44,9 +44,9 @@ Describe "Loading and running PowerShellEditorServices" { AfterAll { try { - $client.Dispose() $psesServer.PsesProcess.Kill() $psesServer.PsesProcess.Dispose() + $client.Dispose() } catch { From 37ddedffa2623578abbb40392d4a8bc1dd92e279 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 15 May 2019 14:28:57 -0700 Subject: [PATCH 45/46] Improve testing --- .../EditorServices.Integration.Tests.ps1 | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index cf0c05869..d9fdbeae5 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -41,23 +41,6 @@ Describe "Loading and running PowerShellEditorServices" { $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } - AfterAll { - try - { - $psesServer.PsesProcess.Kill() - $psesServer.PsesProcess.Dispose() - $client.Dispose() - } - catch - { - # Do nothing - } - - # TODO: We shouldn't need to skip this error. - # It's not clear why we get it but it only occurs on Windows - ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) #-IgnoreException 'EndOfStreamException' - } - It "Starts and responds to an initialization request" { $request = Send-LspInitializeRequest -Client $client $response = Get-LspResponse -Client $client -Id $request.Id @@ -74,6 +57,28 @@ Describe "Loading and running PowerShellEditorServices" { # TODO: The server seems to stay up waiting for the debug connection # $psesServer.PsesProcess.HasExited | Should -BeTrue + # We close the process here rather than in an AfterAll + # since errors can occur and we want to test for them. + # Naturally this depends on Pester executing tests in order. + + # We also have to dispose of everything properly, + # which means we have to use these cascading try/finally statements + try + { + $psesServer.PsesProcess.Kill() + } + finally + { + try + { + $psesServer.PsesProcess.Dispose() + } + finally + { + $client.Dispose() + } + } + ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } } From 9f5666f4917eac660c75912861b7b04f2125147c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 15 May 2019 14:50:20 -0700 Subject: [PATCH 46/46] Add comments about test ordering --- test/Pester/EditorServices.Integration.Tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index d9fdbeae5..d5ea605c6 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -41,6 +41,7 @@ Describe "Loading and running PowerShellEditorServices" { $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName } + # This test MUST be first It "Starts and responds to an initialization request" { $request = Send-LspInitializeRequest -Client $client $response = Get-LspResponse -Client $client -Id $request.Id @@ -49,6 +50,7 @@ Describe "Loading and running PowerShellEditorServices" { ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client $response = Get-LspResponse -Client $client -Id $request.Id