Skip to content

Commit 5cf6d15

Browse files
committed
Auto-restart when project does not support Hot Reload
1 parent 39febbb commit 5cf6d15

File tree

23 files changed

+413
-320
lines changed

23 files changed

+413
-320
lines changed

src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
namespace Microsoft.DotNet.HotReload
2222
{
23-
internal sealed class DefaultHotReloadClient(ILogger logger, ILogger agentLogger, string startupHookPath, bool enableStaticAssetUpdates)
23+
internal sealed class DefaultHotReloadClient(ILogger logger, ILogger agentLogger, string startupHookPath, bool handlesStaticAssetUpdates)
2424
: HotReloadClient(logger, agentLogger)
2525
{
2626
private readonly string _namedPipeName = Guid.NewGuid().ToString("N");
@@ -225,7 +225,7 @@ static ImmutableArray<RuntimeManagedCodeUpdate> ToRuntimeUpdates(IEnumerable<Hot
225225

226226
public override async Task<Task<bool>> ApplyStaticAssetUpdatesAsync(ImmutableArray<HotReloadStaticAssetUpdate> updates, CancellationToken processExitedCancellationToken, CancellationToken cancellationToken)
227227
{
228-
if (!enableStaticAssetUpdates)
228+
if (!handlesStaticAssetUpdates)
229229
{
230230
// The client has no concept of static assets.
231231
return Task.FromResult(true);

src/BuiltInTools/HotReloadClient/HotReloadClients.cs

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System;
77
using System.Collections.Generic;
88
using System.Collections.Immutable;
9+
using System.Diagnostics;
910
using System.IO;
1011
using System.Linq;
1112
using System.Runtime.InteropServices;
@@ -16,13 +17,25 @@
1617

1718
namespace Microsoft.DotNet.HotReload;
1819

19-
internal sealed class HotReloadClients(ImmutableArray<(HotReloadClient client, string name)> clients, AbstractBrowserRefreshServer? browserRefreshServer) : IDisposable
20+
/// <summary>
21+
/// Facilitates Hot Reload updates across multiple clients/processes.
22+
/// </summary>
23+
/// <param name="clients">
24+
/// Clients that handle managed updates and static asset updates if <paramref name="useRefreshServerToApplyStaticAssets"/> is false.
25+
/// </param>
26+
/// <param name="browserRefreshServer">
27+
/// Browser refresh server used to communicate managed code update status and errors to the browser,
28+
/// and to apply static asset updates if <paramref name="useRefreshServerToApplyStaticAssets"/> is true.
29+
/// </param>
30+
/// <param name="useRefreshServerToApplyStaticAssets">
31+
/// True to use <paramref name="browserRefreshServer"/> to apply static asset updates (if available).
32+
/// False to use the <paramref name="clients"/> to apply static asset updates.
33+
/// </param>
34+
internal sealed class HotReloadClients(
35+
ImmutableArray<(HotReloadClient client, string name)> clients,
36+
AbstractBrowserRefreshServer? browserRefreshServer,
37+
bool useRefreshServerToApplyStaticAssets) : IDisposable
2038
{
21-
public HotReloadClients(HotReloadClient client, AbstractBrowserRefreshServer? browserRefreshServer)
22-
: this([(client, "")], browserRefreshServer)
23-
{
24-
}
25-
2639
/// <summary>
2740
/// Disposes all clients. Can occur unexpectedly whenever the process exits.
2841
/// </summary>
@@ -34,6 +47,16 @@ public void Dispose()
3447
}
3548
}
3649

50+
/// <summary>
51+
/// True if Hot Reload is implemented via managed agents.
52+
/// The update itself might not be managed code update, it may be a static asset update implemented via a managed agent.
53+
/// </summary>
54+
public bool IsManagedAgentSupported
55+
=> !clients.IsEmpty;
56+
57+
public bool UseRefreshServerToApplyStaticAssets
58+
=> useRefreshServerToApplyStaticAssets;
59+
3760
public AbstractBrowserRefreshServer? BrowserRefreshServer
3861
=> browserRefreshServer;
3962

@@ -59,18 +82,6 @@ public event Action<int, string> OnRuntimeRudeEdit
5982
}
6083
}
6184

62-
/// <summary>
63-
/// All clients share the same loggers.
64-
/// </summary>
65-
public ILogger ClientLogger
66-
=> clients.First().client.Logger;
67-
68-
/// <summary>
69-
/// All clients share the same loggers.
70-
/// </summary>
71-
public ILogger AgentLogger
72-
=> clients.First().client.AgentLogger;
73-
7485
internal void ConfigureLaunchEnvironment(IDictionary<string, string> environmentBuilder)
7586
{
7687
foreach (var (client, _) in clients)
@@ -99,6 +110,12 @@ internal async ValueTask WaitForConnectionEstablishedAsync(CancellationToken can
99110
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
100111
public async ValueTask<ImmutableArray<string>> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken)
101112
{
113+
if (!IsManagedAgentSupported)
114+
{
115+
// empty capabilities will cause rude edit ENC0097: NotSupportedByRuntime.
116+
return [];
117+
}
118+
102119
if (clients is [var (singleClient, _)])
103120
{
104121
return await singleClient.GetUpdateCapabilitiesAsync(cancellationToken);
@@ -114,6 +131,9 @@ public async ValueTask<ImmutableArray<string>> GetUpdateCapabilitiesAsync(Cancel
114131
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
115132
public async Task<Task> ApplyManagedCodeUpdatesAsync(ImmutableArray<HotReloadManagedCodeUpdate> updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken)
116133
{
134+
// shouldn't be called if there are no clients
135+
Debug.Assert(IsManagedAgentSupported);
136+
117137
// Apply to all processes.
118138
// The module the change is for does not need to be loaded to any of the processes, yet we still consider it successful if the application does not fail.
119139
// In each process we store the deltas for application when/if the module is loaded to the process later.
@@ -137,6 +157,9 @@ async Task CompleteApplyOperationAsync()
137157
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
138158
public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellationToken)
139159
{
160+
// shouldn't be called if there are no clients
161+
Debug.Assert(IsManagedAgentSupported);
162+
140163
if (clients is [var (singleClient, _)])
141164
{
142165
await singleClient.InitialUpdatesAppliedAsync(cancellationToken);
@@ -150,23 +173,26 @@ public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellation
150173
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
151174
public async Task<Task> ApplyStaticAssetUpdatesAsync(IEnumerable<StaticWebAsset> assets, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken)
152175
{
153-
if (browserRefreshServer != null)
176+
if (useRefreshServerToApplyStaticAssets)
154177
{
178+
Debug.Assert(browserRefreshServer != null);
155179
return browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.RelativeUrl), applyOperationCancellationToken).AsTask();
156180
}
157181

182+
// shouldn't be called if there are no clients
183+
Debug.Assert(IsManagedAgentSupported);
184+
158185
var updates = new List<HotReloadStaticAssetUpdate>();
159186

160187
foreach (var asset in assets)
161188
{
162189
try
163190
{
164-
ClientLogger.LogDebug("Loading asset '{Url}' from '{Path}'.", asset.RelativeUrl, asset.FilePath);
165191
updates.Add(await HotReloadStaticAssetUpdate.CreateAsync(asset, cancellationToken));
166192
}
167193
catch (Exception e) when (e is not OperationCanceledException)
168194
{
169-
ClientLogger.LogError("Failed to read file {FilePath}: {Message}", asset.FilePath, e.Message);
195+
clients.First().client.Logger.LogError("Failed to read file {FilePath}: {Message}", asset.FilePath, e.Message);
170196
continue;
171197
}
172198
}
@@ -177,6 +203,10 @@ public async Task<Task> ApplyStaticAssetUpdatesAsync(IEnumerable<StaticWebAsset>
177203
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
178204
public async ValueTask<Task> ApplyStaticAssetUpdatesAsync(ImmutableArray<HotReloadStaticAssetUpdate> updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken)
179205
{
206+
// shouldn't be called if there are no clients
207+
Debug.Assert(IsManagedAgentSupported);
208+
Debug.Assert(!useRefreshServerToApplyStaticAssets);
209+
180210
var applyTasks = await Task.WhenAll(clients.Select(c => c.client.ApplyStaticAssetUpdatesAsync(updates, applyOperationCancellationToken, cancellationToken)));
181211

182212
return Task.WhenAll(applyTasks);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
namespace Microsoft.DotNet.HotReload;
7+
8+
internal readonly struct StaticAsset(string filePath, string relativeUrl, string assemblyName, bool isApplicationProject)
9+
{
10+
public string FilePath => filePath;
11+
public string RelativeUrl => relativeUrl;
12+
public string AssemblyName => assemblyName;
13+
public bool IsApplicationProject => isApplicationProject;
14+
}

src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyAppModel.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ internal sealed class BlazorWebAssemblyAppModel(DotNetWatchContext context, Proj
1717
{
1818
public override ProjectGraphNode LaunchingProject => clientProject;
1919

20-
public override bool RequiresBrowserRefresh => true;
20+
public override bool ManagedHotReloadRequiresBrowserRefresh => true;
2121

22-
protected override HotReloadClients CreateClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
22+
protected override ImmutableArray<(HotReloadClient client, string name)> CreateManagedClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
2323
{
2424
Debug.Assert(browserRefreshServer != null);
25-
return new(CreateWebAssemblyClient(clientLogger, agentLogger, browserRefreshServer, clientProject), browserRefreshServer);
25+
return [(CreateWebAssemblyClient(clientLogger, agentLogger, browserRefreshServer, clientProject), "")];
2626
}
2727
}

src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyHostedAppModel.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,16 @@ internal sealed class BlazorWebAssemblyHostedAppModel(DotNetWatchContext context
1919
{
2020
public override ProjectGraphNode LaunchingProject => serverProject;
2121

22-
public override bool RequiresBrowserRefresh => true;
22+
public override bool ManagedHotReloadRequiresBrowserRefresh => true;
2323

24-
protected override HotReloadClients CreateClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
24+
protected override ImmutableArray<(HotReloadClient client, string name)> CreateManagedClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
2525
{
2626
Debug.Assert(browserRefreshServer != null);
2727

28-
return new(
29-
[
30-
(CreateWebAssemblyClient(clientLogger, agentLogger, browserRefreshServer, clientProject), "client"),
31-
(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(serverProject), enableStaticAssetUpdates: false), "host")
32-
],
33-
browserRefreshServer);
28+
return
29+
[
30+
(CreateWebAssemblyClient(clientLogger, agentLogger, browserRefreshServer, clientProject), "client"),
31+
(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(serverProject), handlesStaticAssetUpdates: false), "host")
32+
];
3433
}
3534
}

src/BuiltInTools/Watch/AppModels/DefaultAppModel.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ namespace Microsoft.DotNet.Watch;
1212
/// </summary>
1313
internal sealed class DefaultAppModel(ProjectGraphNode project) : HotReloadAppModel
1414
{
15-
public override ValueTask<HotReloadClients?> TryCreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
16-
=> new(new HotReloadClients(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(project), enableStaticAssetUpdates: true), browserRefreshServer: null));
15+
public override ValueTask<HotReloadClients> CreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
16+
=> new(new HotReloadClients(
17+
clients: IsManagedAgentSupported(project, clientLogger)
18+
? [(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(project), handlesStaticAssetUpdates: true), "")]
19+
: [],
20+
browserRefreshServer: null,
21+
useRefreshServerToApplyStaticAssets: false));
1722
}

src/BuiltInTools/Watch/AppModels/HotReloadAppModel.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.DotNet.Watch;
99

1010
internal abstract partial class HotReloadAppModel()
1111
{
12-
public abstract ValueTask<HotReloadClients?> TryCreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken);
12+
public abstract ValueTask<HotReloadClients> CreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken);
1313

1414
protected static string GetInjectedAssemblyPath(string targetFramework, string assemblyName)
1515
=> Path.Combine(Path.GetDirectoryName(typeof(HotReloadAppModel).Assembly.Location)!, "hotreload", targetFramework, assemblyName + ".dll");
@@ -45,4 +45,42 @@ public static HotReloadAppModel InferFromProject(DotNetWatchContext context, Pro
4545
context.Logger.Log(MessageDescriptor.ApplicationKind_Default);
4646
return new DefaultAppModel(projectNode);
4747
}
48+
49+
/// <summary>
50+
/// True if a managed code agent can be injected into the target process.
51+
/// The agent is injected either via dotnet startup hook, or via web server middleware for WASM clients.
52+
/// </summary>
53+
internal static bool IsManagedAgentSupported(ProjectGraphNode project, ILogger logger)
54+
{
55+
if (!project.IsNetCoreApp(Versions.Version6_0))
56+
{
57+
LogWarning("target framework is older than 6.0");
58+
return false;
59+
}
60+
61+
// If property is not specified startup hook is enabled:
62+
// https://github.com/dotnet/runtime/blob/4b0b7238ba021b610d3963313b4471517108d2bc/src/libraries/System.Private.CoreLib/src/System/StartupHookProvider.cs#L22
63+
// Startup hooks are not used for WASM projects.
64+
//
65+
// TODO: Remove once implemented: https://github.com/dotnet/runtime/issues/123778
66+
if (!project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.StartupHookSupport, defaultValue: true) &&
67+
!project.GetCapabilities().Contains(ProjectCapability.WebAssembly))
68+
{
69+
// Report which property is causing lack of support for startup hooks:
70+
var (propertyName, propertyValue) =
71+
project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.PublishAot)
72+
? (PropertyNames.PublishAot, true)
73+
: project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.PublishTrimmed)
74+
? (PropertyNames.PublishTrimmed, true)
75+
: (PropertyNames.StartupHookSupport, false);
76+
77+
LogWarning(string.Format("'{0}' property is '{1}'", propertyName, propertyValue));
78+
return false;
79+
}
80+
81+
return true;
82+
83+
void LogWarning(string reason)
84+
=> logger.Log(MessageDescriptor.ProjectDoesNotSupportHotReload, reason);
85+
}
4886
}

src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,24 @@ internal abstract class WebApplicationAppModel(DotNetWatchContext context) : Hot
1616

1717
public DotNetWatchContext Context => context;
1818

19-
public abstract bool RequiresBrowserRefresh { get; }
19+
public abstract bool ManagedHotReloadRequiresBrowserRefresh { get; }
2020

2121
/// <summary>
2222
/// Project that's used for launching the application.
2323
/// </summary>
2424
public abstract ProjectGraphNode LaunchingProject { get; }
2525

26-
protected abstract HotReloadClients CreateClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer);
26+
protected abstract ImmutableArray<(HotReloadClient client, string name)> CreateManagedClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer);
2727

28-
public async sealed override ValueTask<HotReloadClients?> TryCreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
28+
public async sealed override ValueTask<HotReloadClients> CreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
2929
{
3030
var browserRefreshServer = await context.BrowserRefreshServerFactory.GetOrCreateBrowserRefreshServerAsync(LaunchingProject, this, cancellationToken);
31-
if (RequiresBrowserRefresh && browserRefreshServer == null)
32-
{
33-
// Error has been reported
34-
return null;
35-
}
3631

37-
return CreateClients(clientLogger, agentLogger, browserRefreshServer);
32+
var managedClients = (!ManagedHotReloadRequiresBrowserRefresh || browserRefreshServer != null) && IsManagedAgentSupported(LaunchingProject, clientLogger)
33+
? CreateManagedClients(clientLogger, agentLogger, browserRefreshServer)
34+
: [];
35+
36+
return new HotReloadClients(managedClients, browserRefreshServer, useRefreshServerToApplyStaticAssets: true);
3837
}
3938

4039
protected WebAssemblyHotReloadClient CreateWebAssemblyClient(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer browserRefreshServer, ProjectGraphNode clientProject)
@@ -71,13 +70,29 @@ public bool IsServerSupported(ProjectGraphNode projectNode, ILogger logger)
7170
{
7271
if (context.EnvironmentOptions.SuppressBrowserRefresh)
7372
{
74-
logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable.WithLevelWhen(LogLevel.Error, RequiresBrowserRefresh), EnvironmentVariables.Names.SuppressBrowserRefresh);
73+
if (ManagedHotReloadRequiresBrowserRefresh)
74+
{
75+
logger.Log(MessageDescriptor.BrowserRefreshSuppressedViaEnvironmentVariable_ApplicationWillBeRestarted, EnvironmentVariables.Names.SuppressBrowserRefresh);
76+
}
77+
else
78+
{
79+
logger.Log(MessageDescriptor.BrowserRefreshSuppressedViaEnvironmentVariable_ManualRefreshRequired, EnvironmentVariables.Names.SuppressBrowserRefresh);
80+
}
81+
7582
return false;
7683
}
7784

7885
if (!projectNode.IsNetCoreApp(minVersion: s_minimumSupportedVersion))
7986
{
80-
logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported.WithLevelWhen(LogLevel.Error, RequiresBrowserRefresh));
87+
if (ManagedHotReloadRequiresBrowserRefresh)
88+
{
89+
logger.Log(MessageDescriptor.BrowserRefreshNotSupportedByProjectTargetFramework_ApplicationWillBeRestarted);
90+
}
91+
else
92+
{
93+
logger.Log(MessageDescriptor.BrowserRefreshNotSupportedByProjectTargetFramework_ManualRefreshRequired);
94+
}
95+
8196
return false;
8297
}
8398

src/BuiltInTools/Watch/AppModels/WebServerAppModel.cs

Lines changed: 4 additions & 3 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.Collections.Immutable;
45
using Microsoft.Build.Graph;
56
using Microsoft.DotNet.HotReload;
67
using Microsoft.Extensions.Logging;
@@ -12,9 +13,9 @@ internal sealed class WebServerAppModel(DotNetWatchContext context, ProjectGraph
1213
{
1314
public override ProjectGraphNode LaunchingProject => serverProject;
1415

15-
public override bool RequiresBrowserRefresh
16+
public override bool ManagedHotReloadRequiresBrowserRefresh
1617
=> false;
1718

18-
protected override HotReloadClients CreateClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
19-
=> new(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(serverProject), enableStaticAssetUpdates: true), browserRefreshServer);
19+
protected override ImmutableArray<(HotReloadClient client, string name)> CreateManagedClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
20+
=> [(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(serverProject), handlesStaticAssetUpdates: true), "")];
2021
}

0 commit comments

Comments
 (0)