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
- using Microsoft . DotNet . Watch ;
6
6
using Microsoft . DotNet . HotReload ;
7
7
8
8
/// <summary>
9
9
/// The runtime startup hook looks for top-level type named "StartupHook".
10
10
/// </summary>
11
11
internal sealed class StartupHook
12
12
{
13
- private static readonly bool s_logToStandardOutput = Environment . GetEnvironmentVariable ( EnvironmentVariables . Names . HotReloadDeltaClientLogMessages ) == "1" ;
14
- private static readonly string s_namedPipeName = Environment . GetEnvironmentVariable ( EnvironmentVariables . Names . DotnetWatchHotReloadNamedPipeName ) ;
13
+ private const int ConnectionTimeoutMS = 5000 ;
14
+
15
+ private static readonly bool s_logToStandardOutput = Environment . GetEnvironmentVariable ( AgentEnvironmentVariables . HotReloadDeltaClientLogMessages ) == "1" ;
16
+ private static readonly string s_namedPipeName = Environment . GetEnvironmentVariable ( AgentEnvironmentVariables . DotNetWatchHotReloadNamedPipeName ) ;
15
17
16
18
/// <summary>
17
19
/// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS.
@@ -22,65 +24,148 @@ public static void Initialize()
22
24
23
25
Log ( $ "Loaded into process: { processPath } ") ;
24
26
25
- ClearHotReloadEnvironmentVariables ( ) ;
27
+ HotReloadAgent . ClearHotReloadEnvironmentVariables ( typeof ( StartupHook ) ) ;
28
+
29
+ Log ( $ "Connecting to hot-reload server") ;
26
30
27
- _ = Task . Run ( async ( ) =>
31
+ // Connect to the pipe synchronously.
32
+ //
33
+ // If a debugger is attached and there is a breakpoint in the startup code connecting asynchronously would
34
+ // set up a race between this code connecting to the server, and the breakpoint being hit. If the breakpoint
35
+ // hits first, applying changes will throw an error that the client is not connected.
36
+ //
37
+ // Updates made before the process is launched need to be applied before loading the affected modules.
38
+
39
+ var pipeClient = new NamedPipeClientStream ( "." , s_namedPipeName , PipeDirection . InOut , PipeOptions . CurrentUserOnly | PipeOptions . Asynchronous ) ;
40
+ try
41
+ {
42
+ pipeClient . Connect ( ConnectionTimeoutMS ) ;
43
+ Log ( "Connected." ) ;
44
+ }
45
+ catch ( TimeoutException )
28
46
{
29
- Log ( $ "Connecting to hot-reload server") ;
47
+ Log ( $ "Failed to connect in { ConnectionTimeoutMS } ms.") ;
48
+ return ;
49
+ }
30
50
31
- const int TimeOutMS = 5000 ;
51
+ var agent = new HotReloadAgent ( ) ;
52
+ try
53
+ {
54
+ // block until initialization completes:
55
+ InitializeAsync ( pipeClient , agent , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( ) ;
32
56
33
- using var pipeClient = new NamedPipeClientStream ( "." , s_namedPipeName , PipeDirection . InOut , PipeOptions . CurrentUserOnly | PipeOptions . Asynchronous ) ;
34
- try
35
- {
36
- await pipeClient . ConnectAsync ( TimeOutMS ) ;
37
- Log ( "Connected." ) ;
38
- }
39
- catch ( TimeoutException )
40
- {
41
- Log ( $ "Failed to connect in { TimeOutMS } ms.") ;
42
- return ;
43
- }
57
+ // fire and forget:
58
+ _ = ReceiveAndApplyUpdatesAsync ( pipeClient , agent , initialUpdates : false , CancellationToken . None ) ;
59
+ }
60
+ catch ( Exception ex )
61
+ {
62
+ Log ( ex . Message ) ;
63
+ pipeClient . Dispose ( ) ;
64
+ }
65
+ }
44
66
45
- using var agent = new HotReloadAgent ( ) ;
46
- try
47
- {
48
- agent . Reporter . Report ( "Writing capabilities: " + agent . Capabilities , AgentMessageSeverity . Verbose ) ;
67
+ private static async ValueTask InitializeAsync ( NamedPipeClientStream pipeClient , HotReloadAgent agent , CancellationToken cancellationToken )
68
+ {
69
+ agent . Reporter . Report ( "Writing capabilities: " + agent . Capabilities , AgentMessageSeverity . Verbose ) ;
49
70
50
- var initPayload = new ClientInitializationRequest ( agent . Capabilities ) ;
51
- await initPayload . WriteAsync ( pipeClient , CancellationToken . None ) ;
71
+ var initPayload = new ClientInitializationResponse ( agent . Capabilities ) ;
72
+ await initPayload . WriteAsync ( pipeClient , cancellationToken ) ;
73
+
74
+ // Apply updates made before this process was launched to avoid executing unupdated versions of the affected modules.
75
+ await ReceiveAndApplyUpdatesAsync ( pipeClient , agent , initialUpdates : true , cancellationToken ) ;
76
+ }
52
77
53
- while ( pipeClient . IsConnected )
78
+ private static async Task ReceiveAndApplyUpdatesAsync ( NamedPipeClientStream pipeClient , HotReloadAgent agent , bool initialUpdates , CancellationToken cancellationToken )
79
+ {
80
+ try
81
+ {
82
+ while ( pipeClient . IsConnected )
83
+ {
84
+ var payloadType = ( RequestType ) await pipeClient . ReadByteAsync ( cancellationToken ) ;
85
+ switch ( payloadType )
54
86
{
55
- var update = await ManagedCodeUpdateRequest . ReadAsync ( pipeClient , CancellationToken . None ) ;
56
- Log ( $ "ResponseLoggingLevel = { update . ResponseLoggingLevel } ") ;
57
-
58
- bool success ;
59
- try
60
- {
61
- agent . ApplyDeltas ( update . Deltas ) ;
62
- success = true ;
63
- }
64
- catch ( Exception e )
65
- {
66
- agent . Reporter . Report ( $ "The runtime failed to applying the change: { e . Message } ", AgentMessageSeverity . Error ) ;
67
- agent . Reporter . Report ( "Further changes won't be applied to this process." , AgentMessageSeverity . Warning ) ;
68
- success = false ;
69
- }
70
-
71
- var logEntries = agent . GetAndClearLogEntries ( update . ResponseLoggingLevel ) ;
72
-
73
- var response = new UpdateResponse ( logEntries , success ) ;
74
- await response . WriteAsync ( pipeClient , CancellationToken . None ) ;
87
+ case RequestType . ManagedCodeUpdate :
88
+ // Shouldn't get initial managed code updates when the debugger is attached.
89
+ // The debugger itself applies these updates when launching process with the debugger attached.
90
+ Debug . Assert ( ! Debugger . IsAttached ) ;
91
+ await ReadAndApplyManagedCodeUpdateAsync ( pipeClient , agent , cancellationToken ) ;
92
+ break ;
93
+
94
+ case RequestType . StaticAssetUpdate :
95
+ await ReadAndApplyStaticAssetUpdateAsync ( pipeClient , agent , cancellationToken ) ;
96
+ break ;
97
+
98
+ case RequestType . InitialUpdatesCompleted when initialUpdates :
99
+ return ;
100
+
101
+ default :
102
+ // can't continue, the pipe content is in an unknown state
103
+ Log ( $ "Unexpected payload type: { payloadType } . Terminating agent.") ;
104
+ return ;
75
105
}
76
106
}
77
- catch ( Exception e )
107
+ }
108
+ catch ( Exception ex )
109
+ {
110
+ Log ( ex . Message ) ;
111
+ }
112
+ finally
113
+ {
114
+ if ( ! pipeClient . IsConnected )
115
+ {
116
+ await pipeClient . DisposeAsync ( ) ;
117
+ }
118
+
119
+ if ( ! initialUpdates )
78
120
{
79
- Log ( e . ToString ( ) ) ;
121
+ agent . Dispose ( ) ;
80
122
}
123
+ }
124
+ }
125
+
126
+ private static async ValueTask ReadAndApplyManagedCodeUpdateAsync (
127
+ NamedPipeClientStream pipeClient ,
128
+ HotReloadAgent agent ,
129
+ CancellationToken cancellationToken )
130
+ {
131
+ var request = await ManagedCodeUpdateRequest . ReadAsync ( pipeClient , cancellationToken ) ;
81
132
82
- Log ( "Stopped received delta updates. Server is no longer connected." ) ;
83
- } ) ;
133
+ bool success ;
134
+ try
135
+ {
136
+ agent . ApplyDeltas ( request . Deltas ) ;
137
+ success = true ;
138
+ }
139
+ catch ( Exception e )
140
+ {
141
+ agent . Reporter . Report ( $ "The runtime failed to applying the change: { e . Message } ", AgentMessageSeverity . Error ) ;
142
+ agent . Reporter . Report ( "Further changes won't be applied to this process." , AgentMessageSeverity . Warning ) ;
143
+ success = false ;
144
+ }
145
+
146
+ var logEntries = agent . GetAndClearLogEntries ( request . ResponseLoggingLevel ) ;
147
+
148
+ var response = new UpdateResponse ( logEntries , success ) ;
149
+ await response . WriteAsync ( pipeClient , cancellationToken ) ;
150
+ }
151
+
152
+ private static async ValueTask ReadAndApplyStaticAssetUpdateAsync (
153
+ NamedPipeClientStream pipeClient ,
154
+ HotReloadAgent agent ,
155
+ CancellationToken cancellationToken )
156
+ {
157
+ var request = await StaticAssetUpdateRequest . ReadAsync ( pipeClient , cancellationToken ) ;
158
+
159
+ agent . ApplyStaticAssetUpdate ( new StaticAssetUpdate ( request . AssemblyName , request . RelativePath , request . Contents , request . IsApplicationProject ) ) ;
160
+
161
+ var logEntries = agent . GetAndClearLogEntries ( request . ResponseLoggingLevel ) ;
162
+
163
+ // Updating static asset only invokes ContentUpdate metadata update handlers.
164
+ // Failures of these handlers are reported to the log and ignored.
165
+ // Therefore, this request always succeeds.
166
+ var response = new UpdateResponse ( logEntries , success : true ) ;
167
+
168
+ await response . WriteAsync ( pipeClient , cancellationToken ) ;
84
169
}
85
170
86
171
public static bool IsMatchingProcess ( string processPath , string targetProcessPath )
@@ -101,32 +186,6 @@ public static bool IsMatchingProcess(string processPath, string targetProcessPat
101
186
string . Equals ( processPath [ ..^ 4 ] , targetProcessPath [ ..^ 4 ] , comparison ) ;
102
187
}
103
188
104
- internal static void ClearHotReloadEnvironmentVariables ( )
105
- {
106
- // Clear any hot-reload specific environment variables. This prevents child processes from being
107
- // affected by the current app's hot reload settings. See https://github.com/dotnet/runtime/issues/58000
108
-
109
- Environment . SetEnvironmentVariable ( EnvironmentVariables . Names . DotnetStartupHooks ,
110
- RemoveCurrentAssembly ( Environment . GetEnvironmentVariable ( EnvironmentVariables . Names . DotnetStartupHooks ) ) ) ;
111
-
112
- Environment . SetEnvironmentVariable ( EnvironmentVariables . Names . DotnetWatchHotReloadNamedPipeName , "" ) ;
113
- Environment . SetEnvironmentVariable ( EnvironmentVariables . Names . HotReloadDeltaClientLogMessages , "" ) ;
114
- }
115
-
116
- internal static string RemoveCurrentAssembly ( string environment )
117
- {
118
- if ( environment is "" )
119
- {
120
- return environment ;
121
- }
122
-
123
- var assemblyLocation = typeof ( StartupHook ) . Assembly . Location ;
124
- var updatedValues = environment . Split ( Path . PathSeparator , StringSplitOptions . RemoveEmptyEntries )
125
- . Where ( e => ! string . Equals ( e , assemblyLocation , StringComparison . OrdinalIgnoreCase ) ) ;
126
-
127
- return string . Join ( Path . PathSeparator , updatedValues ) ;
128
- }
129
-
130
189
private static void Log ( string message )
131
190
{
132
191
if ( s_logToStandardOutput )
0 commit comments