Skip to content

Commit ff98a90

Browse files
committed
Auto-restart project on runtime rude edit
1 parent 35066e5 commit ff98a90

File tree

17 files changed

+445
-71
lines changed

17 files changed

+445
-71
lines changed

src/BuiltInTools/DotNetDeltaApplier/PipeListener.cs

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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;
56
using System.Reflection;
67
using System.Runtime.Loader;
@@ -9,6 +10,16 @@ namespace Microsoft.DotNet.HotReload;
910

1011
internal 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+
private NamedPipeClientStream? _pipeClient;
22+
1223
public Task Listen(CancellationToken cancellationToken)
1324
{
1425
// Connect to the pipe synchronously.
@@ -21,23 +32,23 @@ public Task Listen(CancellationToken cancellationToken)
2132

2233
log($"Connecting to hot-reload server via pipe {pipeName}");
2334

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);
2536
try
2637
{
27-
pipeClient.Connect(connectionTimeoutMS);
38+
_pipeClient.Connect(connectionTimeoutMS);
2839
log("Connected.");
2940
}
3041
catch (TimeoutException)
3142
{
3243
log($"Failed to connect in {connectionTimeoutMS}ms.");
33-
pipeClient.Dispose();
44+
_pipeClient.Dispose();
3445
return Task.CompletedTask;
3546
}
3647

3748
try
3849
{
3950
// block execution of the app until initial updates are applied:
40-
InitializeAsync(pipeClient, cancellationToken).GetAwaiter().GetResult();
51+
InitializeAsync(cancellationToken).GetAwaiter().GetResult();
4152
}
4253
catch (Exception e)
4354
{
@@ -46,7 +57,8 @@ public Task Listen(CancellationToken cancellationToken)
4657
log(e.Message);
4758
}
4859

49-
pipeClient.Dispose();
60+
_pipeClient.Dispose();
61+
_pipeClient = null;
5062
agent.Dispose();
5163

5264
return Task.CompletedTask;
@@ -56,48 +68,53 @@ 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();
80+
_pipeClient = null;
6881
agent.Dispose();
6982
}
7083
}, cancellationToken);
7184
}
7285

73-
private async Task InitializeAsync(NamedPipeClientStream pipeClient, CancellationToken cancellationToken)
86+
private async Task InitializeAsync(CancellationToken cancellationToken)
7487
{
88+
Debug.Assert(_pipeClient != null);
89+
7590
agent.Reporter.Report("Writing capabilities: " + agent.Capabilities, AgentMessageSeverity.Verbose);
7691

7792
var initPayload = new ClientInitializationResponse(agent.Capabilities);
78-
await initPayload.WriteAsync(pipeClient, cancellationToken);
93+
await initPayload.WriteAsync(_pipeClient, cancellationToken);
7994

8095
// Apply updates made before this process was launched to avoid executing unupdated versions of the affected modules.
8196

8297
// We should only receive ManagedCodeUpdate when when the debugger isn't attached,
8398
// otherwise the initialization should send InitialUpdatesCompleted immediately.
8499
// 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);
86101
}
87102

88-
private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient, bool initialUpdates, CancellationToken cancellationToken)
103+
private async Task ReceiveAndApplyUpdatesAsync(bool initialUpdates, CancellationToken cancellationToken)
89104
{
90-
while (pipeClient.IsConnected)
105+
Debug.Assert(_pipeClient != null);
106+
107+
while (_pipeClient.IsConnected)
91108
{
92-
var payloadType = (RequestType)await pipeClient.ReadByteAsync(cancellationToken);
109+
var payloadType = (RequestType)await _pipeClient.ReadByteAsync(cancellationToken);
93110
switch (payloadType)
94111
{
95112
case RequestType.ManagedCodeUpdate:
96-
await ReadAndApplyManagedCodeUpdateAsync(pipeClient, cancellationToken);
113+
await ReadAndApplyManagedCodeUpdateAsync(cancellationToken);
97114
break;
98115

99116
case RequestType.StaticAssetUpdate:
100-
await ReadAndApplyStaticAssetUpdateAsync(pipeClient, cancellationToken);
117+
await ReadAndApplyStaticAssetUpdateAsync(cancellationToken);
101118
break;
102119

103120
case RequestType.InitialUpdatesCompleted when initialUpdates:
@@ -110,11 +127,11 @@ private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient,
110127
}
111128
}
112129

113-
private async ValueTask ReadAndApplyManagedCodeUpdateAsync(
114-
NamedPipeClientStream pipeClient,
115-
CancellationToken cancellationToken)
130+
private async ValueTask ReadAndApplyManagedCodeUpdateAsync(CancellationToken cancellationToken)
116131
{
117-
var request = await ManagedCodeUpdateRequest.ReadAsync(pipeClient, cancellationToken);
132+
Debug.Assert(_pipeClient != null);
133+
134+
var request = await ManagedCodeUpdateRequest.ReadAsync(_pipeClient, cancellationToken);
118135

119136
bool success;
120137
try
@@ -131,15 +148,14 @@ private async ValueTask ReadAndApplyManagedCodeUpdateAsync(
131148

132149
var logEntries = agent.Reporter.GetAndClearLogEntries(request.ResponseLoggingLevel);
133150

134-
var response = new UpdateResponse(logEntries, success);
135-
await response.WriteAsync(pipeClient, cancellationToken);
151+
await SendResponseAsync(new UpdateResponse(logEntries, success), cancellationToken);
136152
}
137153

138-
private async ValueTask ReadAndApplyStaticAssetUpdateAsync(
139-
NamedPipeClientStream pipeClient,
140-
CancellationToken cancellationToken)
154+
private async ValueTask ReadAndApplyStaticAssetUpdateAsync(CancellationToken cancellationToken)
141155
{
142-
var request = await StaticAssetUpdateRequest.ReadAsync(pipeClient, cancellationToken);
156+
Debug.Assert(_pipeClient != null);
157+
158+
var request = await StaticAssetUpdateRequest.ReadAsync(_pipeClient, cancellationToken);
143159

144160
try
145161
{
@@ -155,8 +171,22 @@ private async ValueTask ReadAndApplyStaticAssetUpdateAsync(
155171
// Updating static asset only invokes ContentUpdate metadata update handlers.
156172
// Failures of these handlers are reported to the log and ignored.
157173
// Therefore, this request always succeeds.
158-
var response = new UpdateResponse(logEntries, success: true);
174+
await SendResponseAsync(new UpdateResponse(logEntries, success: true), cancellationToken);
175+
}
159176

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+
}
161191
}
162192
}

src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,49 @@ public static void Initialize()
4040

4141
RegisterSignalHandlers();
4242

43-
var agent = new HotReloadAgent(assemblyResolvingHandler: (_, args) =>
44-
{
45-
Log($"Resolving '{args.Name}, Version={args.Version}'");
46-
var path = Path.Combine(processDir, args.Name + ".dll");
47-
return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null;
48-
});
43+
PipeListener? listener = null;
44+
45+
var agent = new HotReloadAgent(
46+
assemblyResolvingHandler: (_, args) =>
47+
{
48+
Log($"Resolving '{args.Name}, Version={args.Version}'");
49+
var path = Path.Combine(processDir, args.Name + ".dll");
50+
return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null;
51+
},
52+
hotReloadExceptionCreateHandler: (code, message) =>
53+
{
54+
// Continue executing the code if the debugger is attached.
55+
// It will throw the exception and the debugger will handle it.
56+
if (Debugger.IsAttached)
57+
{
58+
return;
59+
}
60+
61+
Debug.Assert(listener != null);
62+
Log($"Runtime rude edit detected: '{message}'");
63+
64+
SendAndForgetAsync().Wait();
65+
66+
// Handle Ctrl+C to terminate gracefully:
67+
Console.CancelKeyPress += (_, _) => Environment.Exit(0);
68+
69+
// wait for the process to be terminated by the Hot Reload client (other threads might still execute):
70+
Thread.Sleep(Timeout.Infinite);
71+
72+
async Task SendAndForgetAsync()
73+
{
74+
try
75+
{
76+
await listener.SendResponseAsync(new HotReloadExceptionCreatedNotification(code, message), CancellationToken.None);
77+
}
78+
catch
79+
{
80+
// do not crash the app
81+
}
82+
}
83+
});
4984

50-
var listener = new PipeListener(s_namedPipeName, agent, Log);
85+
listener = new PipeListener(s_namedPipeName, agent, Log);
5186

5287
// fire and forget:
5388
_ = listener.Listen(CancellationToken.None);

src/BuiltInTools/HotReloadAgent.PipeRpc/NamedPipeContract.cs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,39 @@
1212

1313
namespace Microsoft.DotNet.HotReload;
1414

15-
internal interface IRequest
15+
internal interface IMessage
1616
{
17-
RequestType Type { get; }
1817
ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken);
1918
}
2019

20+
internal interface IRequest : IMessage
21+
{
22+
RequestType Type { get; }
23+
}
24+
25+
internal interface IResponse : IMessage
26+
{
27+
ResponseType Type { get; }
28+
}
29+
2130
internal interface IUpdateRequest : IRequest
2231
{
2332
}
2433

25-
internal enum RequestType
34+
internal enum RequestType : byte
2635
{
2736
ManagedCodeUpdate = 1,
2837
StaticAssetUpdate = 2,
2938
InitialUpdatesCompleted = 3,
3039
}
3140

41+
internal enum ResponseType : byte
42+
{
43+
InitializationResponse = 1,
44+
UpdateResponse = 2,
45+
HotReloadExceptionNotification = 3,
46+
}
47+
3248
internal readonly struct ManagedCodeUpdateRequest(IReadOnlyList<RuntimeManagedCodeUpdate> updates, ResponseLoggingLevel responseLoggingLevel) : IUpdateRequest
3349
{
3450
private const byte Version = 4;
@@ -81,8 +97,10 @@ public static async ValueTask<ManagedCodeUpdateRequest> ReadAsync(Stream stream,
8197
}
8298
}
8399

84-
internal readonly struct UpdateResponse(IReadOnlyCollection<(string message, AgentMessageSeverity severity)> log, bool success)
100+
internal readonly struct UpdateResponse(IReadOnlyCollection<(string message, AgentMessageSeverity severity)> log, bool success) : IResponse
85101
{
102+
public ResponseType Type => ResponseType.UpdateResponse;
103+
86104
public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
87105
{
88106
await stream.WriteAsync(success, cancellationToken);
@@ -116,10 +134,12 @@ public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationT
116134
}
117135
}
118136

119-
internal readonly struct ClientInitializationResponse(string capabilities)
137+
internal readonly struct ClientInitializationResponse(string capabilities) : IResponse
120138
{
121139
private const byte Version = 0;
122140

141+
public ResponseType Type => ResponseType.InitializationResponse;
142+
123143
public string Capabilities { get; } = capabilities;
124144

125145
public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
@@ -141,6 +161,26 @@ public static async ValueTask<ClientInitializationResponse> ReadAsync(Stream str
141161
}
142162
}
143163

164+
internal readonly struct HotReloadExceptionCreatedNotification(int code, string message) : IResponse
165+
{
166+
public ResponseType Type => ResponseType.HotReloadExceptionNotification;
167+
public int Code => code;
168+
public string Message => message;
169+
170+
public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
171+
{
172+
await stream.WriteAsync(code, cancellationToken);
173+
await stream.WriteAsync(message, cancellationToken);
174+
}
175+
176+
public static async ValueTask<HotReloadExceptionCreatedNotification> ReadAsync(Stream stream, CancellationToken cancellationToken)
177+
{
178+
var code = await stream.ReadInt32Async(cancellationToken);
179+
var message = await stream.ReadStringAsync(cancellationToken);
180+
return new HotReloadExceptionCreatedNotification(code, message);
181+
}
182+
}
183+
144184
internal readonly struct StaticAssetUpdateRequest(
145185
RuntimeStaticAssetUpdate update,
146186
ResponseLoggingLevel responseLoggingLevel) : IUpdateRequest

src/BuiltInTools/HotReloadAgent.WebAssembly.Browser/WebAssemblyHotReload.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ public static async Task InitializeAsync(string baseUri)
7171
{
7272
s_initialized = true;
7373

74-
var agent = new HotReloadAgent(assemblyResolvingHandler: null);
74+
// TODO: Implement hotReloadExceptionCreateHandler: https://github.com/dotnet/sdk/issues/51056
75+
var agent = new HotReloadAgent(assemblyResolvingHandler: null, hotReloadExceptionCreateHandler: null);
7576

7677
var existingAgent = Interlocked.CompareExchange(ref s_hotReloadAgent, agent, null);
7778
if (existingAgent != null)

0 commit comments

Comments
 (0)