1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
+ using System . Diagnostics ;
4
5
using System . IO . Pipes ;
5
6
using System . Reflection ;
6
7
using System . Runtime . Loader ;
@@ -9,6 +10,16 @@ namespace Microsoft.DotNet.HotReload;
9
10
10
11
internal sealed class PipeListener ( string pipeName , IHotReloadAgent agent , Action < string > log , int connectionTimeoutMS = 5000 )
11
12
{
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
+ private NamedPipeClientStream ? _pipeClient ;
22
+
12
23
public Task Listen ( CancellationToken cancellationToken )
13
24
{
14
25
// Connect to the pipe synchronously.
@@ -21,23 +32,23 @@ public Task Listen(CancellationToken cancellationToken)
21
32
22
33
log ( $ "Connecting to hot-reload server via pipe { pipeName } ") ;
23
34
24
- var pipeClient = new NamedPipeClientStream ( serverName : "." , pipeName , PipeDirection . InOut , PipeOptions . CurrentUserOnly | PipeOptions . Asynchronous ) ;
35
+ _pipeClient = new NamedPipeClientStream ( serverName : "." , pipeName , PipeDirection . InOut , PipeOptions . CurrentUserOnly | PipeOptions . Asynchronous ) ;
25
36
try
26
37
{
27
- pipeClient . Connect ( connectionTimeoutMS ) ;
38
+ _pipeClient . Connect ( connectionTimeoutMS ) ;
28
39
log ( "Connected." ) ;
29
40
}
30
41
catch ( TimeoutException )
31
42
{
32
43
log ( $ "Failed to connect in { connectionTimeoutMS } ms.") ;
33
- pipeClient . Dispose ( ) ;
44
+ _pipeClient . Dispose ( ) ;
34
45
return Task . CompletedTask ;
35
46
}
36
47
37
48
try
38
49
{
39
50
// block execution of the app until initial updates are applied:
40
- InitializeAsync ( pipeClient , cancellationToken ) . GetAwaiter ( ) . GetResult ( ) ;
51
+ InitializeAsync ( cancellationToken ) . GetAwaiter ( ) . GetResult ( ) ;
41
52
}
42
53
catch ( Exception e )
43
54
{
@@ -46,7 +57,8 @@ public Task Listen(CancellationToken cancellationToken)
46
57
log ( e . Message ) ;
47
58
}
48
59
49
- pipeClient . Dispose ( ) ;
60
+ _pipeClient . Dispose ( ) ;
61
+ _pipeClient = null ;
50
62
agent . Dispose ( ) ;
51
63
52
64
return Task . CompletedTask ;
@@ -56,48 +68,53 @@ public Task Listen(CancellationToken cancellationToken)
56
68
{
57
69
try
58
70
{
59
- await ReceiveAndApplyUpdatesAsync ( pipeClient , initialUpdates : false , cancellationToken ) ;
71
+ await ReceiveAndApplyUpdatesAsync ( initialUpdates : false , cancellationToken ) ;
60
72
}
61
73
catch ( Exception e ) when ( e is not OperationCanceledException )
62
74
{
63
75
log ( e . Message ) ;
64
76
}
65
77
finally
66
78
{
67
- pipeClient . Dispose ( ) ;
79
+ _pipeClient . Dispose ( ) ;
80
+ _pipeClient = null ;
68
81
agent . Dispose ( ) ;
69
82
}
70
83
} , cancellationToken ) ;
71
84
}
72
85
73
- private async Task InitializeAsync ( NamedPipeClientStream pipeClient , CancellationToken cancellationToken )
86
+ private async Task InitializeAsync ( CancellationToken cancellationToken )
74
87
{
88
+ Debug . Assert ( _pipeClient != null ) ;
89
+
75
90
agent . Reporter . Report ( "Writing capabilities: " + agent . Capabilities , AgentMessageSeverity . Verbose ) ;
76
91
77
92
var initPayload = new ClientInitializationResponse ( agent . Capabilities ) ;
78
- await initPayload . WriteAsync ( pipeClient , cancellationToken ) ;
93
+ await initPayload . WriteAsync ( _pipeClient , cancellationToken ) ;
79
94
80
95
// Apply updates made before this process was launched to avoid executing unupdated versions of the affected modules.
81
96
82
97
// We should only receive ManagedCodeUpdate when when the debugger isn't attached,
83
98
// otherwise the initialization should send InitialUpdatesCompleted immediately.
84
99
// The debugger itself applies these updates when launching process with the debugger attached.
85
- await ReceiveAndApplyUpdatesAsync ( pipeClient , initialUpdates : true , cancellationToken ) ;
100
+ await ReceiveAndApplyUpdatesAsync ( initialUpdates : true , cancellationToken ) ;
86
101
}
87
102
88
- private async Task ReceiveAndApplyUpdatesAsync ( NamedPipeClientStream pipeClient , bool initialUpdates , CancellationToken cancellationToken )
103
+ private async Task ReceiveAndApplyUpdatesAsync ( bool initialUpdates , CancellationToken cancellationToken )
89
104
{
90
- while ( pipeClient . IsConnected )
105
+ Debug . Assert ( _pipeClient != null ) ;
106
+
107
+ while ( _pipeClient . IsConnected )
91
108
{
92
- var payloadType = ( RequestType ) await pipeClient . ReadByteAsync ( cancellationToken ) ;
109
+ var payloadType = ( RequestType ) await _pipeClient . ReadByteAsync ( cancellationToken ) ;
93
110
switch ( payloadType )
94
111
{
95
112
case RequestType . ManagedCodeUpdate :
96
- await ReadAndApplyManagedCodeUpdateAsync ( pipeClient , cancellationToken ) ;
113
+ await ReadAndApplyManagedCodeUpdateAsync ( cancellationToken ) ;
97
114
break ;
98
115
99
116
case RequestType . StaticAssetUpdate :
100
- await ReadAndApplyStaticAssetUpdateAsync ( pipeClient , cancellationToken ) ;
117
+ await ReadAndApplyStaticAssetUpdateAsync ( cancellationToken ) ;
101
118
break ;
102
119
103
120
case RequestType . InitialUpdatesCompleted when initialUpdates :
@@ -110,11 +127,11 @@ private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient,
110
127
}
111
128
}
112
129
113
- private async ValueTask ReadAndApplyManagedCodeUpdateAsync (
114
- NamedPipeClientStream pipeClient ,
115
- CancellationToken cancellationToken )
130
+ private async ValueTask ReadAndApplyManagedCodeUpdateAsync ( CancellationToken cancellationToken )
116
131
{
117
- var request = await ManagedCodeUpdateRequest . ReadAsync ( pipeClient , cancellationToken ) ;
132
+ Debug . Assert ( _pipeClient != null ) ;
133
+
134
+ var request = await ManagedCodeUpdateRequest . ReadAsync ( _pipeClient , cancellationToken ) ;
118
135
119
136
bool success ;
120
137
try
@@ -131,15 +148,14 @@ private async ValueTask ReadAndApplyManagedCodeUpdateAsync(
131
148
132
149
var logEntries = agent . Reporter . GetAndClearLogEntries ( request . ResponseLoggingLevel ) ;
133
150
134
- var response = new UpdateResponse ( logEntries , success ) ;
135
- await response . WriteAsync ( pipeClient , cancellationToken ) ;
151
+ await SendResponseAsync ( new UpdateResponse ( logEntries , success ) , cancellationToken ) ;
136
152
}
137
153
138
- private async ValueTask ReadAndApplyStaticAssetUpdateAsync (
139
- NamedPipeClientStream pipeClient ,
140
- CancellationToken cancellationToken )
154
+ private async ValueTask ReadAndApplyStaticAssetUpdateAsync ( CancellationToken cancellationToken )
141
155
{
142
- var request = await StaticAssetUpdateRequest . ReadAsync ( pipeClient , cancellationToken ) ;
156
+ Debug . Assert ( _pipeClient != null ) ;
157
+
158
+ var request = await StaticAssetUpdateRequest . ReadAsync ( _pipeClient , cancellationToken ) ;
143
159
144
160
try
145
161
{
@@ -155,8 +171,22 @@ private async ValueTask ReadAndApplyStaticAssetUpdateAsync(
155
171
// Updating static asset only invokes ContentUpdate metadata update handlers.
156
172
// Failures of these handlers are reported to the log and ignored.
157
173
// Therefore, this request always succeeds.
158
- var response = new UpdateResponse ( logEntries , success : true ) ;
174
+ await SendResponseAsync ( new UpdateResponse ( logEntries , success : true ) , cancellationToken ) ;
175
+ }
159
176
160
- await response . WriteAsync ( pipeClient , cancellationToken ) ;
177
+ internal async ValueTask SendResponseAsync < T > ( T response , CancellationToken cancellationToken )
178
+ where T : IResponse
179
+ {
180
+ Debug . Assert ( _pipeClient != null ) ;
181
+ try
182
+ {
183
+ _messageToClientLock . Wait ( cancellationToken ) ;
184
+ await _pipeClient . WriteAsync ( ( byte ) response . Type , cancellationToken ) ;
185
+ await response . WriteAsync ( _pipeClient , cancellationToken ) ;
186
+ }
187
+ finally
188
+ {
189
+ _messageToClientLock . Release ( ) ;
190
+ }
161
191
}
162
192
}
0 commit comments