Skip to content

Commit f7c9400

Browse files
committed
Fix a race condition in MessageWriter.WriteMessage
This change fixes a race condition in MessageWriter.WriteMessage where multiple calls to the method would end up writing at the same time. This is due to the use of 'await' calls and happens even when both calls occur on the same synchronization context. The subsequent calls proceed while the first call is awaiting the completion of I/O writes. The solution is to use an AsyncLock to synchronize I/O operations while allowing more messages to be queued up.
1 parent 9785999 commit f7c9400

File tree

2 files changed

+35
-19
lines changed

2 files changed

+35
-19
lines changed

src/PowerShellEditorServices.Host/MessageLoop.cs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public class MessageLoop : IHost
3131
private EditorSession editorSession;
3232
private MessageReader messageReader;
3333
private MessageWriter messageWriter;
34-
private SynchronizationContext syncContext;
34+
private SynchronizationContext applicationSyncContext;
35+
private SynchronizationContext messageLoopSyncContext;
3536
private AsyncContextThread messageLoopThread;
3637

3738
#endregion
@@ -61,7 +62,7 @@ public void Start()
6162
private async Task StartMessageLoop()
6263
{
6364
// Hold on to the current synchronization context
64-
this.syncContext = SynchronizationContext.Current;
65+
this.applicationSyncContext = SynchronizationContext.Current;
6566

6667
// Start the message listener on another thread
6768
this.messageLoopThread = new AsyncContextThread(true);
@@ -70,6 +71,8 @@ private async Task StartMessageLoop()
7071

7172
async Task ListenForMessages()
7273
{
74+
this.messageLoopSyncContext = SynchronizationContext.Current;
75+
7376
// Ensure that the console is using UTF-8 encoding
7477
System.Console.InputEncoding = Encoding.UTF8;
7578
System.Console.OutputEncoding = Encoding.UTF8;
@@ -161,21 +164,26 @@ await this.messageWriter.WriteMessage(
161164

162165
async void DebugService_DebuggerStopped(object sender, DebuggerStopEventArgs e)
163166
{
164-
await this.messageWriter.WriteMessage(
165-
new StoppedEvent
167+
// Push the write operation to the correct thread
168+
this.messageLoopSyncContext.Post(
169+
async (obj) =>
166170
{
167-
Body = new StoppedEventBody
168-
{
169-
Source = new Source
171+
await this.messageWriter.WriteMessage(
172+
new StoppedEvent
170173
{
171-
Path = e.InvocationInfo.ScriptName,
172-
},
173-
Line = e.InvocationInfo.ScriptLineNumber,
174-
Column = e.InvocationInfo.OffsetInLine,
175-
ThreadId = 1, // TODO: Change this based on context
176-
Reason = "breakpoint" // TODO: Change this based on context
177-
}
178-
});
174+
Body = new StoppedEventBody
175+
{
176+
Source = new Source
177+
{
178+
Path = e.InvocationInfo.ScriptName,
179+
},
180+
Line = e.InvocationInfo.ScriptLineNumber,
181+
Column = e.InvocationInfo.OffsetInLine,
182+
ThreadId = 1, // TODO: Change this based on context
183+
Reason = "breakpoint" // TODO: Change this based on context
184+
}
185+
});
186+
}, null);
179187
}
180188

181189
void PowerShellSession_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e)

src/PowerShellEditorServices.Transport.Stdio/Message/MessageWriter.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.PowerShell.EditorServices.Utility;
77
using Newtonsoft.Json;
88
using Newtonsoft.Json.Linq;
9+
using Nito.AsyncEx;
910
using System.IO;
1011
using System.Text;
1112
using System.Threading.Tasks;
@@ -18,6 +19,7 @@ public class MessageWriter
1819

1920
private Stream outputStream;
2021
private MessageTypeResolver messageTypeResolver;
22+
private AsyncLock writeLock = new AsyncLock();
2123

2224
private JsonSerializer loggingSerializer =
2325
JsonSerializer.Create(
@@ -80,10 +82,16 @@ public async Task WriteMessage(MessageBase messageToWrite)
8082
Constants.ContentLengthFormatString,
8183
messageBytes.Length));
8284

83-
// Send the message
84-
await this.outputStream.WriteAsync(headerBytes, 0, headerBytes.Length);
85-
await this.outputStream.WriteAsync(messageBytes, 0, messageBytes.Length);
86-
await this.outputStream.FlushAsync();
85+
// Make sure only one call is writing at a time. You might be thinking
86+
// "Why not use a normal lock?" We use an AsyncLock here so that the
87+
// message loop doesn't get blocked while waiting for I/O to complete.
88+
using (await this.writeLock.LockAsync())
89+
{
90+
// Send the message
91+
await this.outputStream.WriteAsync(headerBytes, 0, headerBytes.Length);
92+
await this.outputStream.WriteAsync(messageBytes, 0, messageBytes.Length);
93+
await this.outputStream.FlushAsync();
94+
}
8795
}
8896

8997
#endregion

0 commit comments

Comments
 (0)