11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using System . Diagnostics ;
45using System . IO . Pipes ;
56using System . Reflection ;
67using System . Runtime . Loader ;
@@ -9,6 +10,17 @@ namespace Microsoft.DotNet.HotReload;
910
1011internal sealed class PipeListener ( string pipeName , IHotReloadAgent agent , Action < string > log , int connectionTimeoutMS = 5000 )
1112{
13+ /// <summary>
14+ /// Messages to the client sent after the initial <see cref="ClientInitializationResponse"/> is sent
15+ /// need to be sent while holding this lock in order to synchronize
16+ /// 1) responses to requests received from the client (e.g. <see cref="UpdateResponse"/>) or
17+ /// 2) notifications sent to the client that may be triggered at arbitrary times (e.g. <see cref="HotReloadExceptionCreatedNotification"/>).
18+ /// </summary>
19+ private readonly SemaphoreSlim _messageToClientLock = new ( initialCount : 1 ) ;
20+
21+ // Not-null once initialized:
22+ private NamedPipeClientStream ? _pipeClient ;
23+
1224 public Task Listen ( CancellationToken cancellationToken )
1325 {
1426 // Connect to the pipe synchronously.
@@ -21,23 +33,23 @@ public Task Listen(CancellationToken cancellationToken)
2133
2234 log ( $ "Connecting to hot-reload server via pipe { pipeName } ") ;
2335
24- var pipeClient = new NamedPipeClientStream ( serverName : "." , pipeName , PipeDirection . InOut , PipeOptions . CurrentUserOnly | PipeOptions . Asynchronous ) ;
36+ _pipeClient = new NamedPipeClientStream ( serverName : "." , pipeName , PipeDirection . InOut , PipeOptions . CurrentUserOnly | PipeOptions . Asynchronous ) ;
2537 try
2638 {
27- pipeClient . Connect ( connectionTimeoutMS ) ;
39+ _pipeClient . Connect ( connectionTimeoutMS ) ;
2840 log ( "Connected." ) ;
2941 }
3042 catch ( TimeoutException )
3143 {
3244 log ( $ "Failed to connect in { connectionTimeoutMS } ms.") ;
33- pipeClient . Dispose ( ) ;
45+ _pipeClient . Dispose ( ) ;
3446 return Task . CompletedTask ;
3547 }
3648
3749 try
3850 {
3951 // block execution of the app until initial updates are applied:
40- InitializeAsync ( pipeClient , cancellationToken ) . GetAwaiter ( ) . GetResult ( ) ;
52+ InitializeAsync ( cancellationToken ) . GetAwaiter ( ) . GetResult ( ) ;
4153 }
4254 catch ( Exception e )
4355 {
@@ -46,7 +58,7 @@ public Task Listen(CancellationToken cancellationToken)
4658 log ( e . Message ) ;
4759 }
4860
49- pipeClient . Dispose ( ) ;
61+ _pipeClient . Dispose ( ) ;
5062 agent . Dispose ( ) ;
5163
5264 return Task . CompletedTask ;
@@ -56,48 +68,52 @@ public Task Listen(CancellationToken cancellationToken)
5668 {
5769 try
5870 {
59- await ReceiveAndApplyUpdatesAsync ( pipeClient , initialUpdates : false , cancellationToken ) ;
71+ await ReceiveAndApplyUpdatesAsync ( initialUpdates : false , cancellationToken ) ;
6072 }
6173 catch ( Exception e ) when ( e is not OperationCanceledException )
6274 {
6375 log ( e . Message ) ;
6476 }
6577 finally
6678 {
67- pipeClient . Dispose ( ) ;
79+ _pipeClient . Dispose ( ) ;
6880 agent . Dispose ( ) ;
6981 }
7082 } , cancellationToken ) ;
7183 }
7284
73- private async Task InitializeAsync ( NamedPipeClientStream pipeClient , CancellationToken cancellationToken )
85+ private async Task InitializeAsync ( CancellationToken cancellationToken )
7486 {
87+ Debug . Assert ( _pipeClient != null ) ;
88+
7589 agent . Reporter . Report ( "Writing capabilities: " + agent . Capabilities , AgentMessageSeverity . Verbose ) ;
7690
7791 var initPayload = new ClientInitializationResponse ( agent . Capabilities ) ;
78- await initPayload . WriteAsync ( pipeClient , cancellationToken ) ;
92+ await initPayload . WriteAsync ( _pipeClient , cancellationToken ) ;
7993
8094 // Apply updates made before this process was launched to avoid executing unupdated versions of the affected modules.
8195
8296 // We should only receive ManagedCodeUpdate when when the debugger isn't attached,
8397 // otherwise the initialization should send InitialUpdatesCompleted immediately.
8498 // The debugger itself applies these updates when launching process with the debugger attached.
85- await ReceiveAndApplyUpdatesAsync ( pipeClient , initialUpdates : true , cancellationToken ) ;
99+ await ReceiveAndApplyUpdatesAsync ( initialUpdates : true , cancellationToken ) ;
86100 }
87101
88- private async Task ReceiveAndApplyUpdatesAsync ( NamedPipeClientStream pipeClient , bool initialUpdates , CancellationToken cancellationToken )
102+ private async Task ReceiveAndApplyUpdatesAsync ( bool initialUpdates , CancellationToken cancellationToken )
89103 {
90- while ( pipeClient . IsConnected )
104+ Debug . Assert ( _pipeClient != null ) ;
105+
106+ while ( _pipeClient . IsConnected )
91107 {
92- var payloadType = ( RequestType ) await pipeClient . ReadByteAsync ( cancellationToken ) ;
108+ var payloadType = ( RequestType ) await _pipeClient . ReadByteAsync ( cancellationToken ) ;
93109 switch ( payloadType )
94110 {
95111 case RequestType . ManagedCodeUpdate :
96- await ReadAndApplyManagedCodeUpdateAsync ( pipeClient , cancellationToken ) ;
112+ await ReadAndApplyManagedCodeUpdateAsync ( cancellationToken ) ;
97113 break ;
98114
99115 case RequestType . StaticAssetUpdate :
100- await ReadAndApplyStaticAssetUpdateAsync ( pipeClient , cancellationToken ) ;
116+ await ReadAndApplyStaticAssetUpdateAsync ( cancellationToken ) ;
101117 break ;
102118
103119 case RequestType . InitialUpdatesCompleted when initialUpdates :
@@ -110,11 +126,11 @@ private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient,
110126 }
111127 }
112128
113- private async ValueTask ReadAndApplyManagedCodeUpdateAsync (
114- NamedPipeClientStream pipeClient ,
115- CancellationToken cancellationToken )
129+ private async ValueTask ReadAndApplyManagedCodeUpdateAsync ( CancellationToken cancellationToken )
116130 {
117- var request = await ManagedCodeUpdateRequest . ReadAsync ( pipeClient , cancellationToken ) ;
131+ Debug . Assert ( _pipeClient != null ) ;
132+
133+ var request = await ManagedCodeUpdateRequest . ReadAsync ( _pipeClient , cancellationToken ) ;
118134
119135 bool success ;
120136 try
@@ -131,15 +147,14 @@ private async ValueTask ReadAndApplyManagedCodeUpdateAsync(
131147
132148 var logEntries = agent . Reporter . GetAndClearLogEntries ( request . ResponseLoggingLevel ) ;
133149
134- var response = new UpdateResponse ( logEntries , success ) ;
135- await response . WriteAsync ( pipeClient , cancellationToken ) ;
150+ await SendResponseAsync ( new UpdateResponse ( logEntries , success ) , cancellationToken ) ;
136151 }
137152
138- private async ValueTask ReadAndApplyStaticAssetUpdateAsync (
139- NamedPipeClientStream pipeClient ,
140- CancellationToken cancellationToken )
153+ private async ValueTask ReadAndApplyStaticAssetUpdateAsync ( CancellationToken cancellationToken )
141154 {
142- var request = await StaticAssetUpdateRequest . ReadAsync ( pipeClient , cancellationToken ) ;
155+ Debug . Assert ( _pipeClient != null ) ;
156+
157+ var request = await StaticAssetUpdateRequest . ReadAsync ( _pipeClient , cancellationToken ) ;
143158
144159 try
145160 {
@@ -155,8 +170,22 @@ private async ValueTask ReadAndApplyStaticAssetUpdateAsync(
155170 // Updating static asset only invokes ContentUpdate metadata update handlers.
156171 // Failures of these handlers are reported to the log and ignored.
157172 // Therefore, this request always succeeds.
158- var response = new UpdateResponse ( logEntries , success : true ) ;
173+ await SendResponseAsync ( new UpdateResponse ( logEntries , success : true ) , cancellationToken ) ;
174+ }
159175
160- await response . WriteAsync ( pipeClient , cancellationToken ) ;
176+ internal async ValueTask SendResponseAsync < T > ( T response , CancellationToken cancellationToken )
177+ where T : IResponse
178+ {
179+ Debug . Assert ( _pipeClient != null ) ;
180+ try
181+ {
182+ await _messageToClientLock . WaitAsync ( cancellationToken ) ;
183+ await _pipeClient . WriteAsync ( ( byte ) response . Type , cancellationToken ) ;
184+ await response . WriteAsync ( _pipeClient , cancellationToken ) ;
185+ }
186+ finally
187+ {
188+ _messageToClientLock . Release ( ) ;
189+ }
161190 }
162191}
0 commit comments