diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index b19caa8bd..46b20884d 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -31,16 +31,21 @@ function Start-EditorServicesHost { [string] $HostVersion, - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] [int] $LanguageServicePort, - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] [int] $DebugServicePort, + [switch] + $Stdio, + + [string] + $LanguageServiceNamedPipe, + + [string] + $DebugServiceNamedPipe, + [ValidateNotNullOrEmpty()] [string] $BundledModulesPath, @@ -89,12 +94,39 @@ function Start-EditorServicesHost { $editorServicesHost.StartLogging($LogPath, $LogLevel); - if ($DebugServiceOnly.IsPresent) { - $editorServicesHost.StartDebugService($DebugServicePort, $profilePaths, $false); + $languageServiceConfig = New-Object Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportConfig + $debugServiceConfig = New-Object Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportConfig + + if ($Stdio.IsPresent) { + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Stdio + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Stdio } - else { - $editorServicesHost.StartLanguageService($LanguageServicePort, $profilePaths); - $editorServicesHost.StartDebugService($DebugServicePort, $profilePaths, $true); + + if ($LanguageServicePort) { + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Tcp + $languageServiceConfig.Endpoint = "$LanguageServicePort" + } + + if ($DebugServicePort) { + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Tcp + $debugServiceConfig.Endpoint = "$DebugServicePort" + } + + if ($LanguageServiceNamedPipe) { + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.Endpoint = "$LanguageServiceNamedPipe" + } + + if ($DebugServiceNamedPipe) { + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.Endpoint = "$DebugServiceNamedPipe" + } + + if ($DebugServiceOnly.IsPresent) { + $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $false); + } else { + $editorServicesHost.StartLanguageService($languageServiceConfig, $profilePaths); + $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $true); } return $editorServicesHost diff --git a/module/Start-EditorServices.ps1 b/module/Start-EditorServices.ps1 index a1f9152ab..c81d92920 100644 --- a/module/Start-EditorServices.ps1 +++ b/module/Start-EditorServices.ps1 @@ -68,7 +68,16 @@ param( $WaitForDebugger, [switch] - $ConfirmInstall + $ConfirmInstall, + + [switch] + $Stdio, + + [string] + $LanguageServicePipeName = $null, + + [string] + $DebugServicePipeName = $null ) $minPortNumber = 10000 @@ -271,16 +280,21 @@ try { Import-Module PowerShellEditorServices -Version $parsedVersion -ErrorAction Stop } - # Locate available port numbers for services - Log "Searching for available socket port for the language service" - $languageServicePort = Get-AvailablePort + # Locate available port numbers for services + # There could be only one service on Stdio channel - Log "Searching for available socket port for the debug service" - $debugServicePort = Get-AvailablePort + $languageServiceTransport = $null + $debugServiceTransport = $null - if (!$languageServicePort -or !$debugServicePort) { - ExitWithError "Failed to find an open socket port for either the language or debug service." - } + if ($Stdio.IsPresent -and -not $DebugServiceOnly.IsPresent) { $languageServiceTransport = "Stdio" } + elseif ($LanguageServicePipeName) { $languageServiceTransport = "NamedPipe"; $languageServicePipeName = "$LanguageServicePipeName" } + elseif ($languageServicePort = Get-AvailablePort) { $languageServiceTransport = "Tcp" } + else { ExitWithError "Failed to find an open socket port for language service." } + + if ($Stdio.IsPresent -and $DebugServiceOnly.IsPresent) { $debugServiceTransport = "Stdio" } + elseif ($DebugServicePipeName) { $debugServiceTransport = "NamedPipe"; $debugServicePipeName = "$DebugServicePipeName" } + elseif ($debugServicePort = Get-AvailablePort) { $debugServiceTransport = "Tcp" } + else { ExitWithError "Failed to find an open socket port for debug service." } if ($EnableConsoleRepl) { Write-Host "PowerShell Integrated Console`n" @@ -298,6 +312,9 @@ try { -AdditionalModules $AdditionalModules ` -LanguageServicePort $languageServicePort ` -DebugServicePort $debugServicePort ` + -LanguageServiceNamedPipe $LanguageServicePipeName ` + -DebugServiceNamedPipe $DebugServicePipeName ` + -Stdio:$Stdio.IsPresent` -BundledModulesPath $BundledModulesPath ` -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` -DebugServiceOnly:$DebugServiceOnly.IsPresent ` @@ -308,10 +325,15 @@ try { $resultDetails = @{ "status" = "started"; - "channel" = "tcp"; - "languageServicePort" = $languageServicePort; - "debugServicePort" = $debugServicePort; - } + "languageServiceTransport" = $languageServiceTransport; + "debugServiceTransport" = $debugServiceTransport; + }; + + if ($languageServicePipeName) { $resultDetails["languageServicePipeName"] = "$languageServicePipeName" } + if ($debugServicePipeName) { $resultDetails["debugServicePipeName"] = "$debugServicePipeName" } + + if ($languageServicePort) { $resultDetails["languageServicePort"] = $languageServicePort } + if ($debugServicePort) { $resultDetails["debugServicePort"] = $debugServicePort } # Notify the client that the services have started WriteSessionFile $resultDetails diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index c0275efce..cb7a0162b 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -29,6 +29,25 @@ public enum EditorServicesHostStatus Ended } + public enum EditorServiceTransportType + { + Tcp, + NamedPipe, + Stdio + } + + public class EditorServiceTransportConfig + { + public EditorServiceTransportType TransportType { get; set; } + /// + /// Configures the endpoint of the transport. + /// For Tcp it's an integer specifying the port. + /// For Stdio it's ignored. + /// For NamedPipe it's the pipe name. + /// + public string Endpoint { get; set; } + } + /// /// Provides a simplified interface for hosting the language and debug services /// over the named pipe server protocol. @@ -48,8 +67,8 @@ public class EditorServicesHost private HashSet featureFlags; private LanguageServer languageServer; - private TcpSocketServerListener languageServiceListener; - private TcpSocketServerListener debugServiceListener; + private IServerListener languageServiceListener; + private IServerListener debugServiceListener; private TaskCompletionSource serverCompletedTask; @@ -164,15 +183,11 @@ public void StartLogging(string logFilePath, LogLevel logLevel) /// /// The port number for the language service. /// The object containing the profile paths to load for this session. - public void StartLanguageService(int languageServicePort, ProfilePaths profilePaths) + public void StartLanguageService(EditorServiceTransportConfig config, ProfilePaths profilePaths) { this.profilePaths = profilePaths; - this.languageServiceListener = - new TcpSocketServerListener( - MessageProtocolType.LanguageServer, - languageServicePort, - this.logger); + this.languageServiceListener = CreateServiceListener(MessageProtocolType.LanguageServer, config); this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnect; this.languageServiceListener.Start(); @@ -180,13 +195,13 @@ public void StartLanguageService(int languageServicePort, ProfilePaths profilePa this.logger.Write( LogLevel.Normal, string.Format( - "Language service started, listening on port {0}", - languageServicePort)); + "Language service started, type = {0}, endpoint = {1}", + config.TransportType, config.Endpoint)); } private async void OnLanguageServiceClientConnect( object sender, - TcpSocketServerChannel serverChannel) + ChannelBase serverChannel) { MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger); @@ -238,27 +253,22 @@ await this.editorSession.PowerShellContext.ImportCommandsModule( /// /// The port number for the debug service. public void StartDebugService( - int debugServicePort, + EditorServiceTransportConfig config, ProfilePaths profilePaths, bool useExistingSession) { - this.debugServiceListener = - new TcpSocketServerListener( - MessageProtocolType.DebugAdapter, - debugServicePort, - this.logger); - + this.debugServiceListener = CreateServiceListener(MessageProtocolType.DebugAdapter, config); this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect; this.debugServiceListener.Start(); this.logger.Write( LogLevel.Normal, string.Format( - "Debug service started, listening on port {0}", - debugServicePort)); + "Debug service started, type = {0}, endpoint = {1}", + config.TransportType, config.Endpoint)); } - private void OnDebugServiceClientConnect(object sender, TcpSocketServerChannel serverChannel) + private void OnDebugServiceClientConnect(object sender, ChannelBase serverChannel) { MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger); @@ -441,6 +451,31 @@ private void CurrentDomain_UnhandledException( e.ExceptionObject.ToString())); } #endif + private IServerListener CreateServiceListener(MessageProtocolType protocol, EditorServiceTransportConfig config) + { + switch (config.TransportType) + { + case EditorServiceTransportType.Tcp: + { + return new TcpSocketServerListener(protocol, int.Parse(config.Endpoint), this.logger); + } + + case EditorServiceTransportType.Stdio: + { + return new StdioServerListener(protocol, this.logger); + } + + case EditorServiceTransportType.NamedPipe: + { + return new NamedPipeServerListener(protocol, config.Endpoint, this.logger); + } + + default: + { + throw new NotSupportedException(); + } + } + } #endregion } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs index cf1cb3e4c..1f17b749c 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs @@ -38,6 +38,7 @@ public override void Start() 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + ListenForConnection(); } catch (IOException e) { diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs index de2b034f0..433f6aabb 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs @@ -8,7 +8,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel { - public abstract class ServerListenerBase + public abstract class ServerListenerBase : IServerListener where TChannel : ChannelBase { private MessageProtocolType messageProtocolType; @@ -22,7 +22,7 @@ public ServerListenerBase(MessageProtocolType messageProtocolType) public abstract void Stop(); - public event EventHandler ClientConnect; + public event EventHandler ClientConnect; protected void OnClientConnect(TChannel channel) { @@ -30,4 +30,13 @@ protected void OnClientConnect(TChannel channel) this.ClientConnect?.Invoke(this, channel); } } + + public interface IServerListener + { + void Start(); + + void Stop(); + + event EventHandler ClientConnect; + } } \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index f551d4884..a31173457 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1362,13 +1362,16 @@ private static FileChange GetFileChangeDetails(Range changeRange, string insertS { // The protocol's positions are zero-based so add 1 to all offsets + if (changeRange == null) return new FileChange { InsertString = insertString, IsReload = true }; + return new FileChange { InsertString = insertString, Line = changeRange.Start.Line + 1, Offset = changeRange.Start.Character + 1, EndLine = changeRange.End.Line + 1, - EndOffset = changeRange.End.Character + 1 + EndOffset = changeRange.End.Character + 1, + IsReload = false }; } diff --git a/src/PowerShellEditorServices/Workspace/FileChange.cs b/src/PowerShellEditorServices/Workspace/FileChange.cs index 3bec7982e..79f6925ea 100644 --- a/src/PowerShellEditorServices/Workspace/FileChange.cs +++ b/src/PowerShellEditorServices/Workspace/FileChange.cs @@ -34,5 +34,12 @@ public class FileChange /// The 1-based column offset where the change ends. /// public int EndOffset { get; set; } + + /// + /// Indicates that the InsertString is an overwrite + /// of the content, and all stale content and metadata + /// should be discarded. + /// + public bool IsReload { get; set; } } } diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs index 19060fb01..fc290cff0 100644 --- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs +++ b/src/PowerShellEditorServices/Workspace/ScriptFile.cs @@ -63,7 +63,7 @@ public string Id /// /// Gets a string containing the full contents of the file. /// - public string Contents + public string Contents { get { @@ -175,7 +175,7 @@ public ScriptFile( /// The path at which the script file resides. /// The path which the client uses to identify the file. /// The version of PowerShell for which the script is being parsed. - public ScriptFile ( + public ScriptFile( string filePath, string clientFilePath, Version powerShellVersion) @@ -184,9 +184,9 @@ public ScriptFile ( clientFilePath, File.ReadAllText(filePath), powerShellVersion) - { + { - } + } #endregion @@ -315,52 +315,64 @@ public void ValidatePosition(int line, int column) /// The FileChange to apply to the file's contents. public void ApplyChange(FileChange fileChange) { - this.ValidatePosition(fileChange.Line, fileChange.Offset); - this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset); - // Break up the change lines string[] changeLines = fileChange.InsertString.Split('\n'); - // Get the first fragment of the first line - string firstLineFragment = + if (fileChange.IsReload) + { + this.FileLines.Clear(); + foreach (var changeLine in changeLines) + { + this.FileLines.Add(changeLine); + } + } + else + { + this.ValidatePosition(fileChange.Line, fileChange.Offset); + this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset); + + // Get the first fragment of the first line + string firstLineFragment = this.FileLines[fileChange.Line - 1] .Substring(0, fileChange.Offset - 1); - // Get the last fragment of the last line - string endLine = this.FileLines[fileChange.EndLine - 1]; - string lastLineFragment = + // Get the last fragment of the last line + string endLine = this.FileLines[fileChange.EndLine - 1]; + string lastLineFragment = endLine.Substring( - fileChange.EndOffset - 1, + fileChange.EndOffset - 1, (this.FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1); - // Remove the old lines - for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++) - { - this.FileLines.RemoveAt(fileChange.Line - 1); - } - - // Build and insert the new lines - int currentLineNumber = fileChange.Line; - for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++) - { - // Since we split the lines above using \n, make sure to - // trim the ending \r's off as well. - string finalLine = changeLines[changeIndex].TrimEnd('\r'); - - // Should we add first or last line fragments? - if (changeIndex == 0) + // Remove the old lines + for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++) { - // Append the first line fragment - finalLine = firstLineFragment + finalLine; + this.FileLines.RemoveAt(fileChange.Line - 1); } - if (changeIndex == changeLines.Length - 1) + + // Build and insert the new lines + int currentLineNumber = fileChange.Line; + for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++) { - // Append the last line fragment - finalLine = finalLine + lastLineFragment; + // Since we split the lines above using \n, make sure to + // trim the ending \r's off as well. + string finalLine = changeLines[changeIndex].TrimEnd('\r'); + + // Should we add first or last line fragments? + if (changeIndex == 0) + { + // Append the first line fragment + finalLine = firstLineFragment + finalLine; + } + if (changeIndex == changeLines.Length - 1) + { + // Append the last line fragment + finalLine = finalLine + lastLineFragment; + } + + this.FileLines.Insert(currentLineNumber - 1, finalLine); + currentLineNumber++; } - this.FileLines.Insert(currentLineNumber - 1, finalLine); - currentLineNumber++; } // Parse the script again to be up-to-date @@ -381,12 +393,12 @@ public int GetOffsetAtPosition(int lineNumber, int columnNumber) int offset = 0; - for(int i = 0; i < lineNumber; i++) + for (int i = 0; i < lineNumber; i++) { if (i == lineNumber - 1) { // Subtract 1 to account for 1-based column numbering - offset += columnNumber - 1; + offset += columnNumber - 1; } else { @@ -430,7 +442,7 @@ public FilePosition CalculatePosition( /// A new BufferPosition containing the position of the offset. public BufferPosition GetPositionAtOffset(int bufferOffset) { - BufferRange bufferRange = + BufferRange bufferRange = GetRangeBetweenOffsets( bufferOffset, bufferOffset); @@ -572,7 +584,7 @@ private void ParseFileContents() var parseError = new ParseError( null, - ex.ErrorRecord.FullyQualifiedErrorId, + ex.ErrorRecord.FullyQualifiedErrorId, ex.Message); parseErrors = new[] { parseError }; @@ -585,12 +597,12 @@ private void ParseFileContents() parseErrors .Select(ScriptFileMarker.FromParseError) .ToArray(); - + //Get all dot sourced referenced files and store them this.ReferencedFiles = AstOperations.FindDotSourcedIncludes(this.ScriptAst); } -#endregion + #endregion } } diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index 741b69abe..e02f6f8d2 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -14,6 +14,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +#if CoreCLR +using System.Reflection; +#endif namespace Microsoft.PowerShell.EditorServices.Test.Host {