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()