Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0488a37

Browse files
committedJun 2, 2017
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.
1 parent 59db8dd commit 0488a37

18 files changed

+391
-277
lines changed
 

‎src/PowerShellEditorServices.Host/EditorServicesHost.cs

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6-
using Microsoft.PowerShell.EditorServices.Console;
6+
using Microsoft.PowerShell.EditorServices.Extensions;
7+
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
78
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
89
using Microsoft.PowerShell.EditorServices.Protocol.Server;
910
using Microsoft.PowerShell.EditorServices.Session;
@@ -12,10 +13,8 @@
1213
using System.Collections.Generic;
1314
using System.Diagnostics;
1415
using System.Management.Automation.Runspaces;
15-
using System.Management.Automation.Host;
1616
using System.Reflection;
17-
using System.Threading;
18-
using Microsoft.PowerShell.EditorServices.Extensions;
17+
using System.Threading.Tasks;
1918

2019
namespace Microsoft.PowerShell.EditorServices.Host
2120
{
@@ -36,12 +35,18 @@ public class EditorServicesHost
3635

3736
private bool enableConsoleRepl;
3837
private HostDetails hostDetails;
38+
private ProfilePaths profilePaths;
3939
private string bundledModulesPath;
4040
private DebugAdapter debugAdapter;
4141
private EditorSession editorSession;
4242
private HashSet<string> featureFlags;
4343
private LanguageServer languageServer;
4444

45+
private TcpSocketServerListener languageServiceListener;
46+
private TcpSocketServerListener debugServiceListener;
47+
48+
private TaskCompletionSource<bool> serverCompletedTask;
49+
4550
#endregion
4651

4752
#region Properties
@@ -152,25 +157,40 @@ public void StartLogging(string logFilePath, LogLevel logLevel)
152157
/// <param name="languageServicePort">The port number for the language service.</param>
153158
/// <param name="profilePaths">The object containing the profile paths to load for this session.</param>
154159
public void StartLanguageService(int languageServicePort, ProfilePaths profilePaths)
160+
{
161+
this.profilePaths = profilePaths;
162+
163+
this.languageServiceListener =
164+
new TcpSocketServerListener(
165+
MessageProtocolType.LanguageServer,
166+
languageServicePort);
167+
168+
this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnect;
169+
this.languageServiceListener.Start();
170+
171+
Logger.Write(
172+
LogLevel.Normal,
173+
string.Format(
174+
"Language service started, listening on port {0}",
175+
languageServicePort));
176+
}
177+
178+
private async void OnLanguageServiceClientConnect(
179+
object sender,
180+
TcpSocketServerChannel serverChannel)
155181
{
156182
this.editorSession =
157183
CreateSession(
158184
this.hostDetails,
159-
profilePaths,
185+
this.profilePaths,
160186
this.enableConsoleRepl);
161187

162188
this.languageServer =
163189
new LanguageServer(
164190
this.editorSession,
165-
new TcpSocketServerChannel(languageServicePort));
166-
167-
this.languageServer.Start().Wait();
191+
serverChannel);
168192

169-
Logger.Write(
170-
LogLevel.Normal,
171-
string.Format(
172-
"Language service started, listening on port {0}",
173-
languageServicePort));
193+
await this.languageServer.Start();
174194
}
175195

176196
/// <summary>
@@ -182,12 +202,29 @@ public void StartDebugService(
182202
ProfilePaths profilePaths,
183203
bool useExistingSession)
184204
{
185-
if (this.enableConsoleRepl && useExistingSession)
205+
this.debugServiceListener =
206+
new TcpSocketServerListener(
207+
MessageProtocolType.LanguageServer,
208+
debugServicePort);
209+
210+
this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect;
211+
this.debugServiceListener.Start();
212+
213+
Logger.Write(
214+
LogLevel.Normal,
215+
string.Format(
216+
"Debug service started, listening on port {0}",
217+
debugServicePort));
218+
}
219+
220+
private async void OnDebugServiceClientConnect(object sender, TcpSocketServerChannel serverChannel)
221+
{
222+
if (this.enableConsoleRepl)
186223
{
187224
this.debugAdapter =
188225
new DebugAdapter(
189226
this.editorSession,
190-
new TcpSocketServerChannel(debugServicePort),
227+
serverChannel,
191228
false);
192229
}
193230
else
@@ -196,42 +233,26 @@ public void StartDebugService(
196233
this.CreateDebugSession(
197234
this.hostDetails,
198235
profilePaths,
199-
this.languageServer.EditorOperations);
236+
this.languageServer?.EditorOperations);
200237

201238
this.debugAdapter =
202239
new DebugAdapter(
203240
debugSession,
204-
new TcpSocketServerChannel(debugServicePort),
241+
serverChannel,
205242
true);
206243
}
207244

208245
this.debugAdapter.SessionEnded +=
209246
(obj, args) =>
210247
{
211-
// Only restart if we're reusing the existing session
212-
// or if we're not using the console REPL, otherwise
213-
// the process should terminate
214-
if (useExistingSession)
215-
{
216-
Logger.Write(
217-
LogLevel.Normal,
218-
"Previous debug session ended, restarting debug service...");
219-
220-
this.StartDebugService(debugServicePort, profilePaths, true);
221-
}
222-
else if (!this.enableConsoleRepl)
223-
{
224-
this.StartDebugService(debugServicePort, profilePaths, false);
225-
}
226-
};
248+
Logger.Write(
249+
LogLevel.Normal,
250+
"Previous debug session ended, restarting debug service listener...");
227251

228-
this.debugAdapter.Start().Wait();
252+
this.debugServiceListener.Start();
253+
};
229254

230-
Logger.Write(
231-
LogLevel.Normal,
232-
string.Format(
233-
"Debug service started, listening on port {0}",
234-
debugServicePort));
255+
await this.debugAdapter.Start();
235256
}
236257

237258
/// <summary>
@@ -251,17 +272,9 @@ public void StopServices()
251272
/// </summary>
252273
public void WaitForCompletion()
253274
{
254-
// Wait based on which server is started. If the language server
255-
// hasn't been started then we may only need to wait on the debug
256-
// adapter to complete.
257-
if (this.languageServer != null)
258-
{
259-
this.languageServer.WaitForExit();
260-
}
261-
else if (this.debugAdapter != null)
262-
{
263-
this.debugAdapter.WaitForExit();
264-
}
275+
// TODO: We need a way to know when to complete this task!
276+
this.serverCompletedTask = new TaskCompletionSource<bool>();
277+
this.serverCompletedTask.Task.Wait();
265278
}
266279

267280
#endregion

‎src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ await this.SendRequest(
3232
}
3333

3434
protected override Task OnStart()
35-
{
36-
return Task.FromResult(true);
37-
}
38-
39-
protected override Task OnConnect()
4035
{
4136
// Initialize the debug adapter
4237
return this.SendRequest(

‎src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ protected override Task Initialize()
2828
// Add handlers for common events
2929
this.SetEventHandler(PublishDiagnosticsNotification.Type, HandlePublishDiagnosticsEvent);
3030

31-
return Task.FromResult(true);
32-
}
33-
34-
protected override Task OnConnect()
35-
{
3631
// Send the 'initialize' request and wait for the response
3732
var initializeParams = new InitializeParams
3833
{

‎src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
1414
/// </summary>
1515
public abstract class ChannelBase
1616
{
17-
/// <summary>
18-
/// Gets a boolean that is true if the channel is connected or false if not.
19-
/// </summary>
20-
public bool IsConnected { get; protected set; }
21-
2217
/// <summary>
2318
/// Gets the MessageReader for reading messages from the channel.
2419
/// </summary>
@@ -48,14 +43,6 @@ public void Start(MessageProtocolType messageProtocolType)
4843
this.Initialize(messageSerializer);
4944
}
5045

51-
/// <summary>
52-
/// Returns a Task that allows the consumer of the ChannelBase
53-
/// implementation to wait until a connection has been made to
54-
/// the opposite endpoint whether it's a client or server.
55-
/// </summary>
56-
/// <returns>A Task to be awaited until a connection is made.</returns>
57-
public abstract Task WaitForConnection();
58-
5946
/// <summary>
6047
/// Stops the channel.
6148
/// </summary>

‎src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,51 +11,15 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
1111
{
1212
public class NamedPipeClientChannel : ChannelBase
1313
{
14-
private string pipeName;
1514
private NamedPipeClientStream pipeClient;
1615

17-
public NamedPipeClientChannel(string pipeName)
16+
public NamedPipeClientChannel(NamedPipeClientStream pipeClient)
1817
{
19-
this.pipeName = pipeName;
20-
}
21-
22-
public override async Task WaitForConnection()
23-
{
24-
#if CoreCLR
25-
await this.pipeClient.ConnectAsync();
26-
#else
27-
this.IsConnected = false;
28-
29-
while (!this.IsConnected)
30-
{
31-
try
32-
{
33-
// Wait for 500 milliseconds so that we don't tie up the thread
34-
this.pipeClient.Connect(500);
35-
this.IsConnected = this.pipeClient.IsConnected;
36-
}
37-
catch (TimeoutException)
38-
{
39-
// Connect timed out, wait and try again
40-
await Task.Delay(1000);
41-
continue;
42-
}
43-
}
44-
#endif
45-
46-
// If we've reached this point, we're connected
47-
this.IsConnected = true;
18+
this.pipeClient = pipeClient;
4819
}
4920

5021
protected override void Initialize(IMessageSerializer messageSerializer)
5122
{
52-
this.pipeClient =
53-
new NamedPipeClientStream(
54-
".",
55-
this.pipeName,
56-
PipeDirection.InOut,
57-
PipeOptions.Asynchronous);
58-
5923
this.MessageReader =
6024
new MessageReader(
6125
this.pipeClient,
@@ -74,6 +38,41 @@ protected override void Shutdown()
7438
this.pipeClient.Dispose();
7539
}
7640
}
41+
42+
public static async Task<NamedPipeClientChannel> Connect(
43+
string pipeName,
44+
MessageProtocolType messageProtocolType)
45+
{
46+
var pipeClient =
47+
new NamedPipeClientStream(
48+
".",
49+
pipeName,
50+
PipeDirection.InOut,
51+
PipeOptions.Asynchronous);
52+
53+
#if CoreCLR
54+
await pipeClient.ConnectAsync();
55+
#else
56+
while (!pipeClient.IsConnected)
57+
{
58+
try
59+
{
60+
// Wait for 500 milliseconds so that we don't tie up the thread
61+
pipeClient.Connect(500);
62+
}
63+
catch (TimeoutException)
64+
{
65+
// Connect timed out, wait and try again
66+
await Task.Delay(1000);
67+
continue;
68+
}
69+
}
70+
#endif
71+
var clientChannel = new NamedPipeClientChannel(pipeClient);
72+
clientChannel.Start(messageProtocolType);
73+
74+
return clientChannel;
75+
}
7776
}
7877
}
7978

‎src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,21 @@
44
//
55

66
using Microsoft.PowerShell.EditorServices.Utility;
7-
using System;
8-
using System.IO;
97
using System.IO.Pipes;
10-
using System.Threading.Tasks;
118

129
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
1310
{
1411
public class NamedPipeServerChannel : ChannelBase
1512
{
16-
private string pipeName;
1713
private NamedPipeServerStream pipeServer;
1814

19-
public NamedPipeServerChannel(string pipeName)
15+
public NamedPipeServerChannel(NamedPipeServerStream pipeServer)
2016
{
21-
this.pipeName = pipeName;
22-
}
23-
24-
public override async Task WaitForConnection()
25-
{
26-
#if CoreCLR
27-
await this.pipeServer.WaitForConnectionAsync();
28-
#else
29-
await Task.Factory.FromAsync(this.pipeServer.BeginWaitForConnection, this.pipeServer.EndWaitForConnection, null);
30-
#endif
31-
32-
this.IsConnected = true;
17+
this.pipeServer = pipeServer;
3318
}
3419

3520
protected override void Initialize(IMessageSerializer messageSerializer)
3621
{
37-
try
38-
{
39-
this.pipeServer =
40-
new NamedPipeServerStream(
41-
pipeName,
42-
PipeDirection.InOut,
43-
1,
44-
PipeTransmissionMode.Byte,
45-
PipeOptions.Asynchronous);
46-
}
47-
catch (IOException e)
48-
{
49-
Logger.Write(
50-
LogLevel.Verbose,
51-
"Named pipe server failed to start due to exception:\r\n\r\n" + e.Message);
52-
53-
throw e;
54-
}
55-
5622
this.MessageReader =
5723
new MessageReader(
5824
this.pipeServer,
@@ -66,14 +32,8 @@ protected override void Initialize(IMessageSerializer messageSerializer)
6632

6733
protected override void Shutdown()
6834
{
69-
if (this.pipeServer != null)
70-
{
71-
Logger.Write(LogLevel.Verbose, "Named pipe server shutting down...");
72-
73-
this.pipeServer.Dispose();
74-
75-
Logger.Write(LogLevel.Verbose, "Named pipe server has been disposed.");
76-
}
35+
// The server listener will take care of the pipe server
36+
this.pipeServer = null;
7737
}
7838
}
7939
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using Microsoft.PowerShell.EditorServices.Utility;
7+
using System;
8+
using System.IO;
9+
using System.IO.Pipes;
10+
using System.Threading.Tasks;
11+
12+
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
13+
{
14+
public class NamedPipeServerListener : ServerListenerBase<NamedPipeServerChannel>
15+
{
16+
private string pipeName;
17+
private NamedPipeServerStream pipeServer;
18+
19+
public NamedPipeServerListener(
20+
MessageProtocolType messageProtocolType,
21+
string pipeName)
22+
: base(messageProtocolType)
23+
{
24+
this.pipeName = pipeName;
25+
}
26+
27+
public override void Start()
28+
{
29+
try
30+
{
31+
this.pipeServer =
32+
new NamedPipeServerStream(
33+
pipeName,
34+
PipeDirection.InOut,
35+
1,
36+
PipeTransmissionMode.Byte,
37+
PipeOptions.Asynchronous);
38+
}
39+
catch (IOException e)
40+
{
41+
Logger.Write(
42+
LogLevel.Verbose,
43+
"Named pipe server failed to start due to exception:\r\n\r\n" + e.Message);
44+
45+
throw e;
46+
}
47+
}
48+
49+
public override void Stop()
50+
{
51+
if (this.pipeServer != null)
52+
{
53+
Logger.Write(LogLevel.Verbose, "Named pipe server shutting down...");
54+
55+
this.pipeServer.Dispose();
56+
57+
Logger.Write(LogLevel.Verbose, "Named pipe server has been disposed.");
58+
}
59+
}
60+
61+
private void ListenForConnection()
62+
{
63+
Task.Factory.StartNew(
64+
async () =>
65+
{
66+
try
67+
{
68+
#if CoreCLR
69+
await this.pipeServer.WaitForConnectionAsync();
70+
#else
71+
await Task.Factory.FromAsync(
72+
this.pipeServer.BeginWaitForConnection,
73+
this.pipeServer.EndWaitForConnection, null);
74+
#endif
75+
this.OnClientConnect(
76+
new NamedPipeServerChannel(
77+
this.pipeServer));
78+
}
79+
catch (Exception e)
80+
{
81+
Logger.WriteException(
82+
"An unhandled exception occurred while listening for a named pipe client connection",
83+
e);
84+
85+
throw e;
86+
}
87+
});
88+
}
89+
}
90+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
10+
{
11+
public abstract class ServerListenerBase<TChannel>
12+
where TChannel : ChannelBase
13+
{
14+
private MessageProtocolType messageProtocolType;
15+
16+
public ServerListenerBase(MessageProtocolType messageProtocolType)
17+
{
18+
this.messageProtocolType = messageProtocolType;
19+
}
20+
21+
public abstract void Start();
22+
23+
public abstract void Stop();
24+
25+
public event EventHandler<TChannel> ClientConnect;
26+
27+
protected void OnClientConnect(TChannel channel)
28+
{
29+
channel.Start(this.messageProtocolType);
30+
this.ClientConnect?.Invoke(this, channel);
31+
}
32+
}
33+
}

‎src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6-
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers;
76
using System.Diagnostics;
87
using System.IO;
98
using System.Text;
10-
using System;
11-
using System.Threading.Tasks;
129

1310
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
1411
{
@@ -44,13 +41,18 @@ public StdioClientChannel(
4441

4542
if (serverProcessArguments != null)
4643
{
47-
this.serviceProcessArguments =
44+
this.serviceProcessArguments =
4845
string.Join(
49-
" ",
46+
" ",
5047
serverProcessArguments);
5148
}
5249
}
5350

51+
public StdioClientChannel(Process serviceProcess)
52+
{
53+
this.serviceProcess = serviceProcess;
54+
}
55+
5456
protected override void Initialize(IMessageSerializer messageSerializer)
5557
{
5658
this.serviceProcess = new Process
@@ -71,30 +73,23 @@ protected override void Initialize(IMessageSerializer messageSerializer)
7173

7274
// Start the process
7375
this.serviceProcess.Start();
76+
7477
this.ProcessId = this.serviceProcess.Id;
7578

7679
// Open the standard input/output streams
7780
this.inputStream = this.serviceProcess.StandardOutput.BaseStream;
7881
this.outputStream = this.serviceProcess.StandardInput.BaseStream;
7982

8083
// Set up the message reader and writer
81-
this.MessageReader =
84+
this.MessageReader =
8285
new MessageReader(
8386
this.inputStream,
8487
messageSerializer);
8588

86-
this.MessageWriter =
89+
this.MessageWriter =
8790
new MessageWriter(
8891
this.outputStream,
8992
messageSerializer);
90-
91-
this.IsConnected = true;
92-
}
93-
94-
public override Task WaitForConnection()
95-
{
96-
// We're always connected immediately in the stdio channel
97-
return Task.FromResult(true);
9893
}
9994

10095
protected override void Shutdown()

‎src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6-
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers;
76
using System.IO;
87
using System.Text;
9-
using System;
10-
using System.Threading.Tasks;
118

129
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
1310
{
@@ -34,23 +31,15 @@ protected override void Initialize(IMessageSerializer messageSerializer)
3431
this.outputStream = System.Console.OpenStandardOutput();
3532

3633
// Set up the reader and writer
37-
this.MessageReader =
34+
this.MessageReader =
3835
new MessageReader(
3936
this.inputStream,
4037
messageSerializer);
4138

42-
this.MessageWriter =
39+
this.MessageWriter =
4340
new MessageWriter(
4441
this.outputStream,
4542
messageSerializer);
46-
47-
this.IsConnected = true;
48-
}
49-
50-
public override Task WaitForConnection()
51-
{
52-
// We're always connected immediately in the stdio channel
53-
return Task.FromResult(true);
5443
}
5544

5645
protected override void Shutdown()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System.IO;
7+
8+
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
9+
{
10+
public class StdioServerListener : ServerListenerBase<StdioServerChannel>
11+
{
12+
public StdioServerListener(MessageProtocolType messageProtocolType) :
13+
base(messageProtocolType)
14+
{
15+
}
16+
17+
public override void Start()
18+
{
19+
// Client is connected immediately because stdio
20+
// will buffer all I/O until we get to it
21+
this.OnClientConnect(new StdioServerChannel());
22+
}
23+
24+
public override void Stop()
25+
{
26+
}
27+
}
28+
}

‎src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketClientChannel.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,24 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
1111
{
1212
public class TcpSocketClientChannel : ChannelBase
1313
{
14-
private int portNumber;
1514
private NetworkStream networkStream;
16-
private IMessageSerializer messageSerializer;
1715

18-
public TcpSocketClientChannel(int portNumber)
16+
public TcpSocketClientChannel(TcpClient tcpClient)
1917
{
20-
this.portNumber = portNumber;
18+
this.networkStream = tcpClient.GetStream();
2119
}
2220

23-
public override async Task WaitForConnection()
21+
protected override void Initialize(IMessageSerializer messageSerializer)
2422
{
25-
TcpClient tcpClient = new TcpClient();
26-
await tcpClient.ConnectAsync(IPAddress.Loopback, this.portNumber);
27-
this.networkStream = tcpClient.GetStream();
28-
2923
this.MessageReader =
3024
new MessageReader(
3125
this.networkStream,
32-
this.messageSerializer);
26+
messageSerializer);
3327

3428
this.MessageWriter =
3529
new MessageWriter(
3630
this.networkStream,
37-
this.messageSerializer);
38-
39-
this.IsConnected = true;
40-
}
41-
42-
protected override void Initialize(IMessageSerializer messageSerializer)
43-
{
44-
this.messageSerializer = messageSerializer;
31+
messageSerializer);
4532
}
4633

4734
protected override void Shutdown()
@@ -52,5 +39,18 @@ protected override void Shutdown()
5239
this.networkStream = null;
5340
}
5441
}
42+
43+
public static async Task<TcpSocketClientChannel> Connect(
44+
int portNumber,
45+
MessageProtocolType messageProtocolType)
46+
{
47+
TcpClient tcpClient = new TcpClient();
48+
await tcpClient.ConnectAsync(IPAddress.Loopback, portNumber);
49+
50+
var clientChannel = new TcpSocketClientChannel(tcpClient);
51+
clientChannel.Start(messageProtocolType);
52+
53+
return clientChannel;
54+
}
5555
}
5656
}

‎src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/TcpSocketServerChannel.cs

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,53 +13,33 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
1313
public class TcpSocketServerChannel : ChannelBase
1414
{
1515
private TcpClient tcpClient;
16-
private TcpListener tcpListener;
1716
private NetworkStream networkStream;
18-
private IMessageSerializer messageSerializer;
1917

20-
public TcpSocketServerChannel(int portNumber)
18+
public TcpSocketServerChannel(TcpClient tcpClient)
2119
{
22-
this.tcpListener = new TcpListener(IPAddress.Loopback, portNumber);
23-
this.tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
24-
this.tcpListener.Start();
20+
this.tcpClient = tcpClient;
21+
this.networkStream = this.tcpClient.GetStream();
2522
}
2623

27-
public override async Task WaitForConnection()
24+
protected override void Initialize(IMessageSerializer messageSerializer)
2825
{
29-
this.tcpClient = await this.tcpListener.AcceptTcpClientAsync();
30-
this.networkStream = this.tcpClient.GetStream();
31-
3226
this.MessageReader =
3327
new MessageReader(
3428
this.networkStream,
35-
this.messageSerializer);
29+
messageSerializer);
3630

3731
this.MessageWriter =
3832
new MessageWriter(
3933
this.networkStream,
40-
this.messageSerializer);
41-
42-
this.IsConnected = true;
43-
}
44-
45-
protected override void Initialize(IMessageSerializer messageSerializer)
46-
{
47-
this.messageSerializer = messageSerializer;
34+
messageSerializer);
4835
}
4936

5037
protected override void Shutdown()
5138
{
52-
if (this.tcpListener != null)
53-
{
54-
this.networkStream.Dispose();
55-
this.tcpListener.Stop();
56-
this.tcpListener = null;
57-
58-
Logger.Write(LogLevel.Verbose, "TCP listener has been stopped");
59-
}
6039

6140
if (this.tcpClient != null)
6241
{
42+
this.networkStream.Dispose();
6343
#if CoreCLR
6444
this.tcpClient.Dispose();
6545
#else
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using System.Net;
8+
using System.Net.Sockets;
9+
using System.Threading.Tasks;
10+
using Microsoft.PowerShell.EditorServices.Utility;
11+
12+
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
13+
{
14+
public class TcpSocketServerListener : ServerListenerBase<TcpSocketServerChannel>
15+
{
16+
private int portNumber;
17+
private TcpListener tcpListener;
18+
19+
public TcpSocketServerListener(
20+
MessageProtocolType messageProtocolType,
21+
int portNumber)
22+
: base(messageProtocolType)
23+
{
24+
this.portNumber = portNumber;
25+
}
26+
27+
public override void Start()
28+
{
29+
if (this.tcpListener == null)
30+
{
31+
this.tcpListener = new TcpListener(IPAddress.Loopback, this.portNumber);
32+
this.tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
33+
this.tcpListener.Start();
34+
}
35+
36+
this.ListenForConnection();
37+
}
38+
39+
public override void Stop()
40+
{
41+
if (this.tcpListener != null)
42+
{
43+
this.tcpListener.Stop();
44+
this.tcpListener = null;
45+
46+
Logger.Write(LogLevel.Verbose, "TCP listener has been stopped");
47+
}
48+
}
49+
50+
private void ListenForConnection()
51+
{
52+
Task.Factory.StartNew(
53+
async () =>
54+
{
55+
try
56+
{
57+
TcpClient tcpClient = await this.tcpListener.AcceptTcpClientAsync();
58+
this.OnClientConnect(
59+
new TcpSocketServerChannel(
60+
tcpClient));
61+
}
62+
catch (Exception e)
63+
{
64+
Logger.WriteException(
65+
"An unhandled exception occurred while listening for a TCP client connection",
66+
e);
67+
68+
throw e;
69+
}
70+
});
71+
}
72+
}
73+
}

‎src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,12 @@ public async Task Start()
9494
// Listen for unhandled exceptions from the message loop
9595
this.UnhandledException += MessageDispatcher_UnhandledException;
9696

97+
// Start the message loop
98+
this.StartMessageLoop();
99+
97100
// Notify implementation about endpoint start
98101
await this.OnStart();
99102

100-
// Wait for connection and notify the implementor
101-
// NOTE: This task is not meant to be awaited.
102-
Task waitTask =
103-
this.protocolChannel
104-
.WaitForConnection()
105-
.ContinueWith(
106-
async (t) =>
107-
{
108-
// Start the MessageDispatcher
109-
this.StartMessageLoop();
110-
await this.OnConnect();
111-
});
112-
113103
// Endpoint is now started
114104
this.currentState = ProtocolEndpointState.Started;
115105
}
@@ -183,11 +173,6 @@ public async Task<TResult> SendRequest<TParams, TResult, TError, TRegistrationOp
183173
return default(TResult);
184174
}
185175

186-
if (!this.protocolChannel.IsConnected)
187-
{
188-
throw new InvalidOperationException("SendRequest called when ProtocolChannel was not yet connected");
189-
}
190-
191176
this.currentMessageId++;
192177

193178
TaskCompletionSource<Message> responseTask = null;
@@ -240,11 +225,6 @@ public Task SendEvent<TParams, TRegistrationOptions>(
240225
return Task.FromResult(true);
241226
}
242227

243-
if (!this.protocolChannel.IsConnected)
244-
{
245-
throw new InvalidOperationException("SendEvent called when ProtocolChannel was not yet connected");
246-
}
247-
248228
// Some events could be raised from a different thread.
249229
// To ensure that messages are written serially, dispatch
250230
// dispatch the SendEvent call to the message loop thread.
@@ -361,11 +341,6 @@ protected virtual Task OnStart()
361341
return Task.FromResult(true);
362342
}
363343

364-
protected virtual Task OnConnect()
365-
{
366-
return Task.FromResult(true);
367-
}
368-
369344
protected virtual Task OnStop()
370345
{
371346
return Task.FromResult(true);

‎src/PowerShellEditorServices/Session/RemoteFileManager.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ public RemoteFileManager(
105105
IEditorOperations editorOperations)
106106
{
107107
Validate.IsNotNull(nameof(powerShellContext), powerShellContext);
108-
Validate.IsNotNull(nameof(editorOperations), editorOperations);
109108

110109
this.powerShellContext = powerShellContext;
111110
this.powerShellContext.RunspaceChanged += HandleRunspaceChanged;
@@ -385,7 +384,7 @@ private async void HandleRunspaceChanged(object sender, RunspaceChangedEventArgs
385384
{
386385
foreach (string remotePath in remotePathMappings.OpenedPaths)
387386
{
388-
await this.editorOperations.CloseFile(remotePath);
387+
await this.editorOperations?.CloseFile(remotePath);
389388
}
390389
}
391390
}
@@ -428,7 +427,7 @@ private void HandlePSEventReceived(object sender, PSEventArgs args)
428427
}
429428

430429
// Open the file in the editor
431-
this.editorOperations.OpenFile(localFilePath);
430+
this.editorOperations?.OpenFile(localFilePath);
432431
}
433432
}
434433
catch (NullReferenceException e)

‎test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
using Microsoft.PowerShell.EditorServices.Protocol.Client;
77
using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter;
8+
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
89
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
10+
using Microsoft.PowerShell.EditorServices.Utility;
911
using System;
1012
using System.IO;
1113
using System.Threading.Tasks;
@@ -30,8 +32,13 @@ public async Task InitializeAsync()
3032
#endif
3133
"logs",
3234
this.GetType().Name,
33-
Guid.NewGuid().ToString().Substring(0, 8) + ".log");
35+
Guid.NewGuid().ToString().Substring(0, 8));
3436

37+
Logger.Initialize(
38+
testLogPath + "-client.log",
39+
LogLevel.Verbose);
40+
41+
testLogPath += "-server.log";
3542
System.Console.WriteLine(" Output log at path: {0}", testLogPath);
3643

3744
Tuple<int, int> portNumbers =
@@ -43,16 +50,11 @@ await this.LaunchService(
4350
this.protocolClient =
4451
this.debugAdapterClient =
4552
new DebugAdapterClient(
46-
new TcpSocketClientChannel(
47-
portNumbers.Item2));
53+
await TcpSocketClientChannel.Connect(
54+
portNumbers.Item2,
55+
MessageProtocolType.DebugAdapter));
4856

4957
await this.debugAdapterClient.Start();
50-
51-
// HACK: Insert a short delay to give the MessageDispatcher time to
52-
// start up. This will have to be fixed soon with a larger refactoring
53-
// to improve the client/server model. Tracking this here:
54-
// https://github.com/PowerShell/PowerShellEditorServices/issues/245
55-
await Task.Delay(1750);
5658
}
5759

5860
public async Task DisposeAsync()

‎test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.PowerShell.EditorServices.Protocol.Messages;
1212
using Microsoft.PowerShell.EditorServices.Protocol.Server;
1313
using Microsoft.PowerShell.EditorServices.Session;
14+
using Microsoft.PowerShell.EditorServices.Utility;
1415
using System;
1516
using System.IO;
1617
using System.Linq;
@@ -34,8 +35,13 @@ public async Task InitializeAsync()
3435
#endif
3536
"logs",
3637
this.GetType().Name,
37-
Guid.NewGuid().ToString().Substring(0, 8) + ".log");
38+
Guid.NewGuid().ToString().Substring(0, 8));
3839

40+
Logger.Initialize(
41+
testLogPath + "-client.log",
42+
LogLevel.Verbose);
43+
44+
testLogPath += "-server.log";
3945
System.Console.WriteLine(" Output log at path: {0}", testLogPath);
4046

4147
Tuple<int, int> portNumbers =
@@ -47,16 +53,11 @@ await this.LaunchService(
4753
this.protocolClient =
4854
this.languageServiceClient =
4955
new LanguageServiceClient(
50-
new TcpSocketClientChannel(
51-
portNumbers.Item1));
56+
await TcpSocketClientChannel.Connect(
57+
portNumbers.Item1,
58+
MessageProtocolType.LanguageServer));
5259

5360
await this.languageServiceClient.Start();
54-
55-
// HACK: Insert a short delay to give the MessageDispatcher time to
56-
// start up. This will have to be fixed soon with a larger refactoring
57-
// to improve the client/server model. Tracking this here:
58-
// https://github.com/PowerShell/PowerShellEditorServices/issues/245
59-
await Task.Delay(1750);
6061
}
6162

6263
public async Task DisposeAsync()

0 commit comments

Comments
 (0)
Please sign in to comment.