From 90afbeb689c7ddb8be360de8bbf14c7fd694725d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 May 2017 11:13:13 -0700 Subject: [PATCH] Establish new channel connection model using server listeners This change refactors our existing channel model to move all connection logic outside of the ChannelBase implementations so that the language and debug client/service pairs can be simplified. This change also allows us to remove a long-standing hack in our Host unit tests which added an artifical delay to give the channel and MessageDispatcher time to get established. --- .../EditorServicesHost.cs | 113 ++++++++++-------- .../Client/DebugAdapterClientBase.cs | 5 - .../Client/LanguageServiceClient.cs | 5 - .../MessageProtocol/Channel/ChannelBase.cs | 13 -- .../Channel/NamedPipeClientChannel.cs | 75 ++++++------ .../Channel/NamedPipeServerChannel.cs | 48 +------- .../Channel/NamedPipeServerListener.cs | 90 ++++++++++++++ .../Channel/ServerListenerBase.cs | 33 +++++ .../Channel/StdioClientChannel.cs | 25 ++-- .../Channel/StdioServerChannel.cs | 15 +-- .../Channel/StdioServerListener.cs | 28 +++++ .../Channel/TcpSocketClientChannel.cs | 36 +++--- .../Channel/TcpSocketServerChannel.cs | 34 ++---- .../Channel/TcpSocketServerListener.cs | 73 +++++++++++ .../MessageProtocol/ProtocolEndpoint.cs | 31 +---- .../Session/RemoteFileManager.cs | 5 +- .../DebugAdapterTests.cs | 20 ++-- .../LanguageServerTests.cs | 19 +-- 18 files changed, 391 insertions(+), 277 deletions(-) create mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs create mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs create mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerListener.cs create mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerListener.cs diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 080e2709c..dde7a2abe 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -3,7 +3,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; using Microsoft.PowerShell.EditorServices.Protocol.Server; using Microsoft.PowerShell.EditorServices.Session; @@ -12,10 +13,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Management.Automation.Runspaces; -using System.Management.Automation.Host; using System.Reflection; -using System.Threading; -using Microsoft.PowerShell.EditorServices.Extensions; +using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Host { @@ -36,12 +35,18 @@ public class EditorServicesHost private bool enableConsoleRepl; private HostDetails hostDetails; + private ProfilePaths profilePaths; private string bundledModulesPath; private DebugAdapter debugAdapter; private EditorSession editorSession; private HashSet featureFlags; private LanguageServer languageServer; + private TcpSocketServerListener languageServiceListener; + private TcpSocketServerListener debugServiceListener; + + private TaskCompletionSource serverCompletedTask; + #endregion #region Properties @@ -152,25 +157,40 @@ 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) + { + this.profilePaths = profilePaths; + + this.languageServiceListener = + new TcpSocketServerListener( + MessageProtocolType.LanguageServer, + languageServicePort); + + this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnect; + this.languageServiceListener.Start(); + + Logger.Write( + LogLevel.Normal, + string.Format( + "Language service started, listening on port {0}", + languageServicePort)); + } + + private async void OnLanguageServiceClientConnect( + object sender, + TcpSocketServerChannel serverChannel) { this.editorSession = CreateSession( this.hostDetails, - profilePaths, + this.profilePaths, this.enableConsoleRepl); this.languageServer = new LanguageServer( this.editorSession, - new TcpSocketServerChannel(languageServicePort)); - - this.languageServer.Start().Wait(); + serverChannel); - Logger.Write( - LogLevel.Normal, - string.Format( - "Language service started, listening on port {0}", - languageServicePort)); + await this.languageServer.Start(); } /// @@ -182,12 +202,29 @@ public void StartDebugService( ProfilePaths profilePaths, bool useExistingSession) { - if (this.enableConsoleRepl && useExistingSession) + this.debugServiceListener = + new TcpSocketServerListener( + MessageProtocolType.LanguageServer, + debugServicePort); + + this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect; + this.debugServiceListener.Start(); + + Logger.Write( + LogLevel.Normal, + string.Format( + "Debug service started, listening on port {0}", + debugServicePort)); + } + + private async void OnDebugServiceClientConnect(object sender, TcpSocketServerChannel serverChannel) + { + if (this.enableConsoleRepl) { this.debugAdapter = new DebugAdapter( this.editorSession, - new TcpSocketServerChannel(debugServicePort), + serverChannel, false); } else @@ -196,42 +233,26 @@ public void StartDebugService( this.CreateDebugSession( this.hostDetails, profilePaths, - this.languageServer.EditorOperations); + this.languageServer?.EditorOperations); this.debugAdapter = new DebugAdapter( debugSession, - new TcpSocketServerChannel(debugServicePort), + serverChannel, true); } this.debugAdapter.SessionEnded += (obj, args) => { - // Only restart if we're reusing the existing session - // or if we're not using the console REPL, otherwise - // the process should terminate - if (useExistingSession) - { - Logger.Write( - LogLevel.Normal, - "Previous debug session ended, restarting debug service..."); - - this.StartDebugService(debugServicePort, profilePaths, true); - } - else if (!this.enableConsoleRepl) - { - this.StartDebugService(debugServicePort, profilePaths, false); - } - }; + Logger.Write( + LogLevel.Normal, + "Previous debug session ended, restarting debug service listener..."); - this.debugAdapter.Start().Wait(); + this.debugServiceListener.Start(); + }; - Logger.Write( - LogLevel.Normal, - string.Format( - "Debug service started, listening on port {0}", - debugServicePort)); + await this.debugAdapter.Start(); } /// @@ -251,17 +272,9 @@ public void StopServices() /// public void WaitForCompletion() { - // Wait based on which server is started. If the language server - // hasn't been started then we may only need to wait on the debug - // adapter to complete. - if (this.languageServer != null) - { - this.languageServer.WaitForExit(); - } - else if (this.debugAdapter != null) - { - this.debugAdapter.WaitForExit(); - } + // TODO: We need a way to know when to complete this task! + this.serverCompletedTask = new TaskCompletionSource(); + this.serverCompletedTask.Task.Wait(); } #endregion diff --git a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs index f27c1cc80..243543841 100644 --- a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs +++ b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs @@ -32,11 +32,6 @@ await this.SendRequest( } protected override Task OnStart() - { - return Task.FromResult(true); - } - - protected override Task OnConnect() { // Initialize the debug adapter return this.SendRequest( diff --git a/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs b/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs index 63fde920f..6c75eebd5 100644 --- a/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs +++ b/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs @@ -28,11 +28,6 @@ protected override Task Initialize() // Add handlers for common events this.SetEventHandler(PublishDiagnosticsNotification.Type, HandlePublishDiagnosticsEvent); - return Task.FromResult(true); - } - - protected override Task OnConnect() - { // Send the 'initialize' request and wait for the response var initializeParams = new InitializeParams { diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs index 54de0d9cc..ea4922938 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs @@ -14,11 +14,6 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel /// public abstract class ChannelBase { - /// - /// Gets a boolean that is true if the channel is connected or false if not. - /// - public bool IsConnected { get; protected set; } - /// /// Gets the MessageReader for reading messages from the channel. /// @@ -48,14 +43,6 @@ public void Start(MessageProtocolType messageProtocolType) this.Initialize(messageSerializer); } - /// - /// Returns a Task that allows the consumer of the ChannelBase - /// implementation to wait until a connection has been made to - /// the opposite endpoint whether it's a client or server. - /// - /// A Task to be awaited until a connection is made. - public abstract Task WaitForConnection(); - /// /// Stops the channel. /// diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs index 9814c3e44..68fe5387b 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs @@ -11,51 +11,15 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel { public class NamedPipeClientChannel : ChannelBase { - private string pipeName; private NamedPipeClientStream pipeClient; - public NamedPipeClientChannel(string pipeName) + public NamedPipeClientChannel(NamedPipeClientStream pipeClient) { - this.pipeName = pipeName; - } - - public override async Task WaitForConnection() - { -#if CoreCLR - await this.pipeClient.ConnectAsync(); -#else - this.IsConnected = false; - - while (!this.IsConnected) - { - try - { - // Wait for 500 milliseconds so that we don't tie up the thread - this.pipeClient.Connect(500); - this.IsConnected = this.pipeClient.IsConnected; - } - catch (TimeoutException) - { - // Connect timed out, wait and try again - await Task.Delay(1000); - continue; - } - } -#endif - - // If we've reached this point, we're connected - this.IsConnected = true; + this.pipeClient = pipeClient; } protected override void Initialize(IMessageSerializer messageSerializer) { - this.pipeClient = - new NamedPipeClientStream( - ".", - this.pipeName, - PipeDirection.InOut, - PipeOptions.Asynchronous); - this.MessageReader = new MessageReader( this.pipeClient, @@ -74,6 +38,41 @@ protected override void Shutdown() this.pipeClient.Dispose(); } } + + public static async Task Connect( + string pipeName, + MessageProtocolType messageProtocolType) + { + var pipeClient = + new NamedPipeClientStream( + ".", + pipeName, + PipeDirection.InOut, + PipeOptions.Asynchronous); + +#if CoreCLR + await pipeClient.ConnectAsync(); +#else + while (!pipeClient.IsConnected) + { + try + { + // Wait for 500 milliseconds so that we don't tie up the thread + pipeClient.Connect(500); + } + catch (TimeoutException) + { + // Connect timed out, wait and try again + await Task.Delay(1000); + continue; + } + } +#endif + var clientChannel = new NamedPipeClientChannel(pipeClient); + clientChannel.Start(messageProtocolType); + + return clientChannel; + } } } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs index f79e32f7d..080b835eb 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs @@ -4,55 +4,21 @@ // using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.IO; using System.IO.Pipes; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel { public class NamedPipeServerChannel : ChannelBase { - private string pipeName; private NamedPipeServerStream pipeServer; - public NamedPipeServerChannel(string pipeName) + public NamedPipeServerChannel(NamedPipeServerStream pipeServer) { - this.pipeName = pipeName; - } - - public override async Task WaitForConnection() - { -#if CoreCLR - await this.pipeServer.WaitForConnectionAsync(); -#else - await Task.Factory.FromAsync(this.pipeServer.BeginWaitForConnection, this.pipeServer.EndWaitForConnection, null); -#endif - - this.IsConnected = true; + this.pipeServer = pipeServer; } protected override void Initialize(IMessageSerializer messageSerializer) { - try - { - this.pipeServer = - new NamedPipeServerStream( - pipeName, - PipeDirection.InOut, - 1, - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous); - } - catch (IOException e) - { - Logger.Write( - LogLevel.Verbose, - "Named pipe server failed to start due to exception:\r\n\r\n" + e.Message); - - throw e; - } - this.MessageReader = new MessageReader( this.pipeServer, @@ -66,14 +32,8 @@ protected override void Initialize(IMessageSerializer messageSerializer) protected override void Shutdown() { - if (this.pipeServer != null) - { - Logger.Write(LogLevel.Verbose, "Named pipe server shutting down..."); - - this.pipeServer.Dispose(); - - Logger.Write(LogLevel.Verbose, "Named pipe server has been disposed."); - } + // The server listener will take care of the pipe server + this.pipeServer = null; } } } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs new file mode 100644 index 000000000..99dbb00fb --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + public class NamedPipeServerListener : ServerListenerBase + { + private string pipeName; + private NamedPipeServerStream pipeServer; + + public NamedPipeServerListener( + MessageProtocolType messageProtocolType, + string pipeName) + : base(messageProtocolType) + { + this.pipeName = pipeName; + } + + public override void Start() + { + try + { + this.pipeServer = + new NamedPipeServerStream( + pipeName, + PipeDirection.InOut, + 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous); + } + catch (IOException e) + { + Logger.Write( + LogLevel.Verbose, + "Named pipe server failed to start due to exception:\r\n\r\n" + e.Message); + + throw e; + } + } + + public override void Stop() + { + if (this.pipeServer != null) + { + Logger.Write(LogLevel.Verbose, "Named pipe server shutting down..."); + + this.pipeServer.Dispose(); + + Logger.Write(LogLevel.Verbose, "Named pipe server has been disposed."); + } + } + + private void ListenForConnection() + { + Task.Factory.StartNew( + async () => + { + try + { +#if CoreCLR + await this.pipeServer.WaitForConnectionAsync(); +#else + await Task.Factory.FromAsync( + this.pipeServer.BeginWaitForConnection, + this.pipeServer.EndWaitForConnection, null); +#endif + this.OnClientConnect( + new NamedPipeServerChannel( + this.pipeServer)); + } + catch (Exception e) + { + Logger.WriteException( + "An unhandled exception occurred while listening for a named pipe client connection", + e); + + throw e; + } + }); + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs new file mode 100644 index 000000000..de2b034f0 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs @@ -0,0 +1,33 @@ +// +// 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.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + public abstract class ServerListenerBase + where TChannel : ChannelBase + { + private MessageProtocolType messageProtocolType; + + public ServerListenerBase(MessageProtocolType messageProtocolType) + { + this.messageProtocolType = messageProtocolType; + } + + public abstract void Start(); + + public abstract void Stop(); + + public event EventHandler ClientConnect; + + protected void OnClientConnect(TChannel channel) + { + channel.Start(this.messageProtocolType); + this.ClientConnect?.Invoke(this, channel); + } + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs index a6be8cc8f..b262afa57 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs @@ -3,12 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; using System.Diagnostics; using System.IO; using System.Text; -using System; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel { @@ -44,13 +41,18 @@ public StdioClientChannel( if (serverProcessArguments != null) { - this.serviceProcessArguments = + this.serviceProcessArguments = string.Join( - " ", + " ", serverProcessArguments); } } + public StdioClientChannel(Process serviceProcess) + { + this.serviceProcess = serviceProcess; + } + protected override void Initialize(IMessageSerializer messageSerializer) { this.serviceProcess = new Process @@ -71,6 +73,7 @@ protected override void Initialize(IMessageSerializer messageSerializer) // Start the process this.serviceProcess.Start(); + this.ProcessId = this.serviceProcess.Id; // Open the standard input/output streams @@ -78,23 +81,15 @@ protected override void Initialize(IMessageSerializer messageSerializer) this.outputStream = this.serviceProcess.StandardInput.BaseStream; // Set up the message reader and writer - this.MessageReader = + this.MessageReader = new MessageReader( this.inputStream, messageSerializer); - this.MessageWriter = + this.MessageWriter = new MessageWriter( this.outputStream, messageSerializer); - - this.IsConnected = true; - } - - public override Task WaitForConnection() - { - // We're always connected immediately in the stdio channel - return Task.FromResult(true); } protected override void Shutdown() diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs index bba6f1bc2..9431639cf 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs @@ -3,11 +3,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; using System.IO; using System.Text; -using System; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel { @@ -34,23 +31,15 @@ protected override void Initialize(IMessageSerializer messageSerializer) this.outputStream = System.Console.OpenStandardOutput(); // Set up the reader and writer - this.MessageReader = + this.MessageReader = new MessageReader( this.inputStream, messageSerializer); - this.MessageWriter = + this.MessageWriter = new MessageWriter( this.outputStream, messageSerializer); - - this.IsConnected = true; - } - - public override Task WaitForConnection() - { - // We're always connected immediately in the stdio channel - return Task.FromResult(true); } protected override void Shutdown() diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerListener.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerListener.cs new file mode 100644 index 000000000..3097c831a --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerListener.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.IO; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + public class StdioServerListener : ServerListenerBase + { + public StdioServerListener(MessageProtocolType messageProtocolType) : + base(messageProtocolType) + { + } + + public override void Start() + { + // Client is connected immediately because stdio + // will buffer all I/O until we get to it + this.OnClientConnect(new StdioServerChannel()); + } + + public override void Stop() + { + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketClientChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketClientChannel.cs index 3c17b02e4..a0baf9e51 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketClientChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketClientChannel.cs @@ -11,37 +11,24 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel { public class TcpSocketClientChannel : ChannelBase { - private int portNumber; private NetworkStream networkStream; - private IMessageSerializer messageSerializer; - public TcpSocketClientChannel(int portNumber) + public TcpSocketClientChannel(TcpClient tcpClient) { - this.portNumber = portNumber; + this.networkStream = tcpClient.GetStream(); } - public override async Task WaitForConnection() + protected override void Initialize(IMessageSerializer messageSerializer) { - TcpClient tcpClient = new TcpClient(); - await tcpClient.ConnectAsync(IPAddress.Loopback, this.portNumber); - this.networkStream = tcpClient.GetStream(); - this.MessageReader = new MessageReader( this.networkStream, - this.messageSerializer); + messageSerializer); this.MessageWriter = new MessageWriter( this.networkStream, - this.messageSerializer); - - this.IsConnected = true; - } - - protected override void Initialize(IMessageSerializer messageSerializer) - { - this.messageSerializer = messageSerializer; + messageSerializer); } protected override void Shutdown() @@ -52,5 +39,18 @@ protected override void Shutdown() this.networkStream = null; } } + + public static async Task Connect( + int portNumber, + MessageProtocolType messageProtocolType) + { + TcpClient tcpClient = new TcpClient(); + await tcpClient.ConnectAsync(IPAddress.Loopback, portNumber); + + var clientChannel = new TcpSocketClientChannel(tcpClient); + clientChannel.Start(messageProtocolType); + + return clientChannel; + } } } \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs index b7a877692..df702046e 100755 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs @@ -13,53 +13,33 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel public class TcpSocketServerChannel : ChannelBase { private TcpClient tcpClient; - private TcpListener tcpListener; private NetworkStream networkStream; - private IMessageSerializer messageSerializer; - public TcpSocketServerChannel(int portNumber) + public TcpSocketServerChannel(TcpClient tcpClient) { - this.tcpListener = new TcpListener(IPAddress.Loopback, portNumber); - this.tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - this.tcpListener.Start(); + this.tcpClient = tcpClient; + this.networkStream = this.tcpClient.GetStream(); } - public override async Task WaitForConnection() + protected override void Initialize(IMessageSerializer messageSerializer) { - this.tcpClient = await this.tcpListener.AcceptTcpClientAsync(); - this.networkStream = this.tcpClient.GetStream(); - this.MessageReader = new MessageReader( this.networkStream, - this.messageSerializer); + messageSerializer); this.MessageWriter = new MessageWriter( this.networkStream, - this.messageSerializer); - - this.IsConnected = true; - } - - protected override void Initialize(IMessageSerializer messageSerializer) - { - this.messageSerializer = messageSerializer; + messageSerializer); } protected override void Shutdown() { - if (this.tcpListener != null) - { - this.networkStream.Dispose(); - this.tcpListener.Stop(); - this.tcpListener = null; - - Logger.Write(LogLevel.Verbose, "TCP listener has been stopped"); - } if (this.tcpClient != null) { + this.networkStream.Dispose(); #if CoreCLR this.tcpClient.Dispose(); #else diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerListener.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerListener.cs new file mode 100644 index 000000000..bb5266454 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerListener.cs @@ -0,0 +1,73 @@ +// +// 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.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + public class TcpSocketServerListener : ServerListenerBase + { + private int portNumber; + private TcpListener tcpListener; + + public TcpSocketServerListener( + MessageProtocolType messageProtocolType, + int portNumber) + : base(messageProtocolType) + { + this.portNumber = portNumber; + } + + public override void Start() + { + if (this.tcpListener == null) + { + this.tcpListener = new TcpListener(IPAddress.Loopback, this.portNumber); + this.tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + this.tcpListener.Start(); + } + + this.ListenForConnection(); + } + + public override void Stop() + { + if (this.tcpListener != null) + { + this.tcpListener.Stop(); + this.tcpListener = null; + + Logger.Write(LogLevel.Verbose, "TCP listener has been stopped"); + } + } + + private void ListenForConnection() + { + Task.Factory.StartNew( + async () => + { + try + { + TcpClient tcpClient = await this.tcpListener.AcceptTcpClientAsync(); + this.OnClientConnect( + new TcpSocketServerChannel( + tcpClient)); + } + catch (Exception e) + { + Logger.WriteException( + "An unhandled exception occurred while listening for a TCP client connection", + e); + + throw e; + } + }); + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs index 232ca4626..cbb9f60fd 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs @@ -94,22 +94,12 @@ public async Task Start() // Listen for unhandled exceptions from the message loop this.UnhandledException += MessageDispatcher_UnhandledException; + // Start the message loop + this.StartMessageLoop(); + // Notify implementation about endpoint start await this.OnStart(); - // Wait for connection and notify the implementor - // NOTE: This task is not meant to be awaited. - Task waitTask = - this.protocolChannel - .WaitForConnection() - .ContinueWith( - async (t) => - { - // Start the MessageDispatcher - this.StartMessageLoop(); - await this.OnConnect(); - }); - // Endpoint is now started this.currentState = ProtocolEndpointState.Started; } @@ -183,11 +173,6 @@ public async Task SendRequest responseTask = null; @@ -240,11 +225,6 @@ public Task SendEvent( return Task.FromResult(true); } - if (!this.protocolChannel.IsConnected) - { - throw new InvalidOperationException("SendEvent called when ProtocolChannel was not yet connected"); - } - // Some events could be raised from a different thread. // To ensure that messages are written serially, dispatch // dispatch the SendEvent call to the message loop thread. @@ -361,11 +341,6 @@ protected virtual Task OnStart() return Task.FromResult(true); } - protected virtual Task OnConnect() - { - return Task.FromResult(true); - } - protected virtual Task OnStop() { return Task.FromResult(true); diff --git a/src/PowerShellEditorServices/Session/RemoteFileManager.cs b/src/PowerShellEditorServices/Session/RemoteFileManager.cs index b77c0a5e6..e43921b26 100644 --- a/src/PowerShellEditorServices/Session/RemoteFileManager.cs +++ b/src/PowerShellEditorServices/Session/RemoteFileManager.cs @@ -105,7 +105,6 @@ public RemoteFileManager( IEditorOperations editorOperations) { Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - Validate.IsNotNull(nameof(editorOperations), editorOperations); this.powerShellContext = powerShellContext; this.powerShellContext.RunspaceChanged += HandleRunspaceChanged; @@ -385,7 +384,7 @@ private async void HandleRunspaceChanged(object sender, RunspaceChangedEventArgs { foreach (string remotePath in remotePathMappings.OpenedPaths) { - await this.editorOperations.CloseFile(remotePath); + await this.editorOperations?.CloseFile(remotePath); } } } @@ -428,7 +427,7 @@ private void HandlePSEventReceived(object sender, PSEventArgs args) } // Open the file in the editor - this.editorOperations.OpenFile(localFilePath); + this.editorOperations?.OpenFile(localFilePath); } } catch (NullReferenceException e) diff --git a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs index 2d5fc905a..66220b52b 100644 --- a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs +++ b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs @@ -5,7 +5,9 @@ using Microsoft.PowerShell.EditorServices.Protocol.Client; using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using Microsoft.PowerShell.EditorServices.Utility; using System; using System.IO; using System.Threading.Tasks; @@ -30,8 +32,13 @@ public async Task InitializeAsync() #endif "logs", this.GetType().Name, - Guid.NewGuid().ToString().Substring(0, 8) + ".log"); + Guid.NewGuid().ToString().Substring(0, 8)); + Logger.Initialize( + testLogPath + "-client.log", + LogLevel.Verbose); + + testLogPath += "-server.log"; System.Console.WriteLine(" Output log at path: {0}", testLogPath); Tuple portNumbers = @@ -43,16 +50,11 @@ await this.LaunchService( this.protocolClient = this.debugAdapterClient = new DebugAdapterClient( - new TcpSocketClientChannel( - portNumbers.Item2)); + await TcpSocketClientChannel.Connect( + portNumbers.Item2, + MessageProtocolType.DebugAdapter)); await this.debugAdapterClient.Start(); - - // HACK: Insert a short delay to give the MessageDispatcher time to - // start up. This will have to be fixed soon with a larger refactoring - // to improve the client/server model. Tracking this here: - // https://github.com/PowerShell/PowerShellEditorServices/issues/245 - await Task.Delay(1750); } public async Task DisposeAsync() diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index eb7ee2dc9..64d682e05 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -11,6 +11,7 @@ using Microsoft.PowerShell.EditorServices.Protocol.Messages; using Microsoft.PowerShell.EditorServices.Protocol.Server; using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Utility; using System; using System.IO; using System.Linq; @@ -34,8 +35,13 @@ public async Task InitializeAsync() #endif "logs", this.GetType().Name, - Guid.NewGuid().ToString().Substring(0, 8) + ".log"); + Guid.NewGuid().ToString().Substring(0, 8)); + Logger.Initialize( + testLogPath + "-client.log", + LogLevel.Verbose); + + testLogPath += "-server.log"; System.Console.WriteLine(" Output log at path: {0}", testLogPath); Tuple portNumbers = @@ -47,16 +53,11 @@ await this.LaunchService( this.protocolClient = this.languageServiceClient = new LanguageServiceClient( - new TcpSocketClientChannel( - portNumbers.Item1)); + await TcpSocketClientChannel.Connect( + portNumbers.Item1, + MessageProtocolType.LanguageServer)); await this.languageServiceClient.Start(); - - // HACK: Insert a short delay to give the MessageDispatcher time to - // start up. This will have to be fixed soon with a larger refactoring - // to improve the client/server model. Tracking this here: - // https://github.com/PowerShell/PowerShellEditorServices/issues/245 - await Task.Delay(1750); } public async Task DisposeAsync()