Skip to content

Commit 98f1043

Browse files
authored
[dotnet watch] Agent improvements (#45997)
1 parent e1caf34 commit 98f1043

35 files changed

+869
-407
lines changed

src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@
1313
<Nullable>enable</Nullable>
1414
</PropertyGroup>
1515

16-
<ItemGroup>
17-
<Compile Include="..\dotnet-watch\EnvironmentVariables_StartupHook.cs" Link="EnvironmentVariables_StartupHook.cs" />
18-
</ItemGroup>
19-
2016
<ItemGroup>
2117
<InternalsVisibleTo Include="Microsoft.Extensions.DotNetDeltaApplier.Tests" />
2218
</ItemGroup>

src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs

Lines changed: 134 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
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;
45
using System.IO.Pipes;
5-
using Microsoft.DotNet.Watch;
66
using Microsoft.DotNet.HotReload;
77

88
/// <summary>
99
/// The runtime startup hook looks for top-level type named "StartupHook".
1010
/// </summary>
1111
internal sealed class StartupHook
1212
{
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);
1517

1618
/// <summary>
1719
/// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS.
@@ -22,65 +24,148 @@ public static void Initialize()
2224

2325
Log($"Loaded into process: {processPath}");
2426

25-
ClearHotReloadEnvironmentVariables();
27+
HotReloadAgent.ClearHotReloadEnvironmentVariables(typeof(StartupHook));
28+
29+
Log($"Connecting to hot-reload server");
2630

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)
2846
{
29-
Log($"Connecting to hot-reload server");
47+
Log($"Failed to connect in {ConnectionTimeoutMS}ms.");
48+
return;
49+
}
3050

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();
3256

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+
}
4466

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);
4970

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+
}
5277

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)
5486
{
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;
75105
}
76106
}
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)
78120
{
79-
Log(e.ToString());
121+
agent.Dispose();
80122
}
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);
81132

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);
84169
}
85170

86171
public static bool IsMatchingProcess(string processPath, string targetProcessPath)
@@ -101,32 +186,6 @@ public static bool IsMatchingProcess(string processPath, string targetProcessPat
101186
string.Equals(processPath[..^4], targetProcessPath[..^4], comparison);
102187
}
103188

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-
130189
private static void Log(string message)
131190
{
132191
if (s_logToStandardOutput)

src/BuiltInTools/HotReloadAgent.PipeRpc/NamedPipeContract.cs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,29 @@ namespace Microsoft.DotNet.HotReload;
1212

1313
internal interface IRequest
1414
{
15+
RequestType Type { get; }
1516
ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken);
1617
}
1718

19+
internal interface IUpdateRequest : IRequest
20+
{
21+
}
22+
1823
internal enum RequestType
1924
{
2025
ManagedCodeUpdate = 1,
2126
StaticAssetUpdate = 2,
2227
InitialUpdatesCompleted = 3,
2328
}
2429

25-
internal readonly struct ManagedCodeUpdateRequest(IReadOnlyList<UpdateDelta> deltas, ResponseLoggingLevel responseLoggingLevel) : IRequest
30+
internal readonly struct ManagedCodeUpdateRequest(IReadOnlyList<UpdateDelta> deltas, ResponseLoggingLevel responseLoggingLevel) : IUpdateRequest
2631
{
2732
private const byte Version = 4;
2833

2934
public IReadOnlyList<UpdateDelta> Deltas { get; } = deltas;
3035
public ResponseLoggingLevel ResponseLoggingLevel { get; } = responseLoggingLevel;
36+
public RequestType Type => RequestType.ManagedCodeUpdate;
3137

32-
/// <summary>
33-
/// Called by the dotnet-watch.
34-
/// </summary>
3538
public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
3639
{
3740
await stream.WriteAsync(Version, cancellationToken);
@@ -49,9 +52,6 @@ public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationT
4952
await stream.WriteAsync((byte)ResponseLoggingLevel, cancellationToken);
5053
}
5154

52-
/// <summary>
53-
/// Called by delta applier.
54-
/// </summary>
5555
public static async ValueTask<ManagedCodeUpdateRequest> ReadAsync(Stream stream, CancellationToken cancellationToken)
5656
{
5757
var version = await stream.ReadByteAsync(cancellationToken);
@@ -114,25 +114,19 @@ public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationT
114114
}
115115
}
116116

117-
internal readonly struct ClientInitializationRequest(string capabilities) : IRequest
117+
internal readonly struct ClientInitializationResponse(string capabilities)
118118
{
119119
private const byte Version = 0;
120120

121121
public string Capabilities { get; } = capabilities;
122122

123-
/// <summary>
124-
/// Called by delta applier.
125-
/// </summary>
126123
public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
127124
{
128125
await stream.WriteAsync(Version, cancellationToken);
129126
await stream.WriteAsync(Capabilities, cancellationToken);
130127
}
131128

132-
/// <summary>
133-
/// Called by dotnet-watch.
134-
/// </summary>
135-
public static async ValueTask<ClientInitializationRequest> ReadAsync(Stream stream, CancellationToken cancellationToken)
129+
public static async ValueTask<ClientInitializationResponse> ReadAsync(Stream stream, CancellationToken cancellationToken)
136130
{
137131
var version = await stream.ReadByteAsync(cancellationToken);
138132
if (version != Version)
@@ -141,22 +135,26 @@ public static async ValueTask<ClientInitializationRequest> ReadAsync(Stream stre
141135
}
142136

143137
var capabilities = await stream.ReadStringAsync(cancellationToken);
144-
return new ClientInitializationRequest(capabilities);
138+
return new ClientInitializationResponse(capabilities);
145139
}
146140
}
147141

148142
internal readonly struct StaticAssetUpdateRequest(
149143
string assemblyName,
150144
string relativePath,
151145
byte[] contents,
152-
bool isApplicationProject) : IRequest
146+
bool isApplicationProject,
147+
ResponseLoggingLevel responseLoggingLevel) : IUpdateRequest
153148
{
154-
private const byte Version = 1;
149+
private const byte Version = 2;
155150

156151
public string AssemblyName { get; } = assemblyName;
157152
public bool IsApplicationProject { get; } = isApplicationProject;
158153
public string RelativePath { get; } = relativePath;
159154
public byte[] Contents { get; } = contents;
155+
public ResponseLoggingLevel ResponseLoggingLevel { get; } = responseLoggingLevel;
156+
157+
public RequestType Type => RequestType.StaticAssetUpdate;
160158

161159
public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
162160
{
@@ -165,6 +163,7 @@ public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationT
165163
await stream.WriteAsync(IsApplicationProject, cancellationToken);
166164
await stream.WriteAsync(RelativePath, cancellationToken);
167165
await stream.WriteByteArrayAsync(Contents, cancellationToken);
166+
await stream.WriteAsync((byte)ResponseLoggingLevel, cancellationToken);
168167
}
169168

170169
public static async ValueTask<StaticAssetUpdateRequest> ReadAsync(Stream stream, CancellationToken cancellationToken)
@@ -176,14 +175,16 @@ public static async ValueTask<StaticAssetUpdateRequest> ReadAsync(Stream stream,
176175
}
177176

178177
var assemblyName = await stream.ReadStringAsync(cancellationToken);
179-
var isAppProject = await stream.ReadBooleanAsync(cancellationToken);
178+
var isApplicationProject = await stream.ReadBooleanAsync(cancellationToken);
180179
var relativePath = await stream.ReadStringAsync(cancellationToken);
181180
var contents = await stream.ReadByteArrayAsync(cancellationToken);
181+
var responseLoggingLevel = (ResponseLoggingLevel)await stream.ReadByteAsync(cancellationToken);
182182

183183
return new StaticAssetUpdateRequest(
184184
assemblyName: assemblyName,
185185
relativePath: relativePath,
186186
contents: contents,
187-
isApplicationProject: isAppProject);
187+
isApplicationProject,
188+
responseLoggingLevel);
188189
}
189190
}

0 commit comments

Comments
 (0)