Skip to content

Commit b5757e5

Browse files
authored
Use WebAssemblyHotReloadCapabilities project property (#45055)
1 parent 3090832 commit b5757e5

File tree

5 files changed

+68
-111
lines changed

5 files changed

+68
-111
lines changed

src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs

Lines changed: 31 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,27 @@
33

44
using System.Buffers;
55
using System.Collections.Immutable;
6+
using Microsoft.Build.Graph;
67
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
78
using Microsoft.DotNet.HotReload;
89

910
namespace Microsoft.DotNet.Watch
1011
{
11-
internal sealed class BlazorWebAssemblyDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, Version? targetFrameworkVersion) : SingleProcessDeltaApplier(reporter)
12+
internal sealed class BlazorWebAssemblyDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, ProjectGraphNode project) : SingleProcessDeltaApplier(reporter)
1213
{
13-
private const string DefaultCapabilities60 = "Baseline";
14-
private const string DefaultCapabilities70 = "Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes";
15-
private const string DefaultCapabilities80 = "Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType";
14+
private static readonly ImmutableArray<string> s_defaultCapabilities60 =
15+
["Baseline"];
16+
17+
private static readonly ImmutableArray<string> s_defaultCapabilities70 =
18+
["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "NewTypeDefinition", "ChangeCustomAttributes"];
19+
20+
private static readonly ImmutableArray<string> s_defaultCapabilities80 =
21+
["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "NewTypeDefinition", "ChangeCustomAttributes",
22+
"AddInstanceFieldToExistingType", "GenericAddMethodToExistingType", "GenericUpdateMethod", "UpdateParameters", "GenericAddFieldToExistingType"];
23+
24+
private static readonly ImmutableArray<string> s_defaultCapabilities90 =
25+
s_defaultCapabilities80;
1626

17-
private ImmutableArray<string> _cachedCapabilities;
18-
private readonly SemaphoreSlim _capabilityRetrievalSemaphore = new(initialCount: 1);
1927
private int _updateId;
2028

2129
public override void Dispose()
@@ -32,109 +40,31 @@ public override async Task WaitForProcessRunningAsync(CancellationToken cancella
3240
// Alternatively, we could inject agent into blazor-devserver.dll and establish a connection on the named pipe.
3341
=> await browserRefreshServer.WaitForClientConnectionAsync(cancellationToken);
3442

35-
public override async Task<ImmutableArray<string>> GetApplyUpdateCapabilitiesAsync(CancellationToken cancellationToken)
43+
public override Task<ImmutableArray<string>> GetApplyUpdateCapabilitiesAsync(CancellationToken cancellationToken)
3644
{
37-
var cachedCapabilities = _cachedCapabilities;
38-
if (!cachedCapabilities.IsDefault)
39-
{
40-
return cachedCapabilities;
41-
}
45+
var capabilities = project.GetWebAssemblyCapabilities();
4246

43-
await _capabilityRetrievalSemaphore.WaitAsync(cancellationToken);
44-
try
45-
{
46-
if (_cachedCapabilities.IsDefault)
47-
{
48-
_cachedCapabilities = await RetrieveAsync(cancellationToken);
49-
}
50-
}
51-
finally
47+
if (capabilities.IsEmpty)
5248
{
53-
_capabilityRetrievalSemaphore.Release();
54-
}
49+
var targetFramework = project.GetTargetFrameworkVersion();
5550

56-
return _cachedCapabilities;
57-
58-
async Task<ImmutableArray<string>> RetrieveAsync(CancellationToken cancellationToken)
59-
{
60-
var buffer = ArrayPool<byte>.Shared.Rent(32 * 1024);
51+
Reporter.Verbose($"Using capabilities based on target framework: '{targetFramework}'.");
6152

62-
try
53+
capabilities = targetFramework?.Major switch
6354
{
64-
Reporter.Verbose("Connecting to the browser.");
65-
66-
await browserRefreshServer.WaitForClientConnectionAsync(cancellationToken);
67-
68-
string capabilities;
69-
if (browserRefreshServer.Options.TestFlags.HasFlag(TestFlags.MockBrowser))
70-
{
71-
// When testing return default capabilities without connecting to an actual browser.
72-
capabilities = GetDefaultCapabilities(targetFrameworkVersion);
73-
}
74-
else
75-
{
76-
string? capabilityString = null;
77-
78-
await browserRefreshServer.SendAndReceiveAsync(
79-
request: _ => default(JsonGetApplyUpdateCapabilitiesRequest),
80-
response: (value, reporter) =>
81-
{
82-
var str = Encoding.UTF8.GetString(value);
83-
if (str.StartsWith('!'))
84-
{
85-
reporter.Verbose($"Exception while reading WASM runtime capabilities: {str[1..]}");
86-
}
87-
else if (str.Length == 0)
88-
{
89-
reporter.Verbose($"Unable to read WASM runtime capabilities");
90-
}
91-
else if (capabilityString == null)
92-
{
93-
capabilityString = str;
94-
}
95-
else if (capabilityString != str)
96-
{
97-
reporter.Verbose($"Received different capabilities from different browsers:{Environment.NewLine}'{str}'{Environment.NewLine}'{capabilityString}'");
98-
}
99-
},
100-
cancellationToken);
101-
102-
if (capabilityString != null)
103-
{
104-
capabilities = capabilityString;
105-
}
106-
else
107-
{
108-
capabilities = GetDefaultCapabilities(targetFrameworkVersion);
109-
Reporter.Verbose($"Falling back to default WASM capabilities: '{capabilities}'");
110-
}
111-
}
112-
113-
// Capabilities are expressed a space-separated string.
114-
// e.g. https://github.com/dotnet/runtime/blob/14343bdc281102bf6fffa1ecdd920221d46761bc/src/coreclr/System.Private.CoreLib/src/System/Reflection/Metadata/AssemblyExtensions.cs#L87
115-
return capabilities.Split(' ').ToImmutableArray();
116-
}
117-
catch (Exception e) when (!cancellationToken.IsCancellationRequested)
118-
{
119-
Reporter.Error($"Failed to read capabilities: {e.Message}");
120-
121-
// Do not attempt to retrieve capabilities again if it fails once, unless the operation is canceled.
122-
return [];
123-
}
124-
finally
125-
{
126-
ArrayPool<byte>.Shared.Return(buffer);
127-
}
55+
9 => s_defaultCapabilities90,
56+
8 => s_defaultCapabilities80,
57+
7 => s_defaultCapabilities70,
58+
6 => s_defaultCapabilities60,
59+
_ => [],
60+
};
61+
}
62+
else
63+
{
64+
Reporter.Verbose($"Project specifies capabilities.");
12865
}
12966

130-
static string GetDefaultCapabilities(Version? targetFrameworkVersion)
131-
=> targetFrameworkVersion?.Major switch
132-
{
133-
>= 8 => DefaultCapabilities80,
134-
>= 7 => DefaultCapabilities70,
135-
>= 6 => DefaultCapabilities60,
136-
_ => string.Empty,
137-
};
67+
return Task.FromResult(capabilities);
13868
}
13969

14070
public override async Task<ApplyStatus> Apply(ImmutableArray<WatchHotReloadService.Update> updates, CancellationToken cancellationToken)

src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyHostedDeltaApplier.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33

44

55
using System.Collections.Immutable;
6+
using Microsoft.Build.Graph;
67
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
78

89
namespace Microsoft.DotNet.Watch
910
{
10-
internal sealed class BlazorWebAssemblyHostedDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, Version? targetFrameworkVersion) : DeltaApplier(reporter)
11+
internal sealed class BlazorWebAssemblyHostedDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, ProjectGraphNode project) : DeltaApplier(reporter)
1112
{
12-
private readonly BlazorWebAssemblyDeltaApplier _wasmApplier = new(reporter, browserRefreshServer, targetFrameworkVersion);
13+
private readonly BlazorWebAssemblyDeltaApplier _wasmApplier = new(reporter, browserRefreshServer, project);
1314
private readonly DefaultDeltaApplier _hostApplier = new(reporter);
1415

1516
public override void Dispose()

src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
103103
_reporter.Report(MessageDescriptor.HotReloadSessionStarted);
104104
}
105105

106-
private static DeltaApplier CreateDeltaApplier(HotReloadProfile profile, Version? targetFramework, BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
106+
private static DeltaApplier CreateDeltaApplier(HotReloadProfile profile, ProjectGraphNode project, BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
107107
=> profile switch
108108
{
109-
HotReloadProfile.BlazorWebAssembly => new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer!, targetFramework),
110-
HotReloadProfile.BlazorHosted => new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer!, targetFramework),
109+
HotReloadProfile.BlazorWebAssembly => new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer!, project),
110+
HotReloadProfile.BlazorHosted => new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer!, project),
111111
_ => new DefaultDeltaApplier(processReporter),
112112
};
113113

@@ -125,8 +125,7 @@ private static DeltaApplier CreateDeltaApplier(HotReloadProfile profile, Version
125125
{
126126
var projectPath = projectNode.ProjectInstance.FullPath;
127127

128-
var targetFramework = projectNode.GetTargetFrameworkVersion();
129-
var deltaApplier = CreateDeltaApplier(profile, targetFramework, browserRefreshServer, processReporter);
128+
var deltaApplier = CreateDeltaApplier(profile, projectNode, browserRefreshServer, processReporter);
130129
var processExitedSource = new CancellationTokenSource();
131130
var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(processExitedSource.Token, cancellationToken);
132131

src/BuiltInTools/dotnet-watch/Utilities/ProjectGraphNodeExtensions.cs

Lines changed: 4 additions & 0 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.Cli;
67

@@ -17,6 +18,9 @@ public static string GetTargetFramework(this ProjectGraphNode projectNode)
1718
public static Version? GetTargetFrameworkVersion(this ProjectGraphNode projectNode)
1819
=> EnvironmentVariableNames.TryParseTargetFrameworkVersion(projectNode.ProjectInstance.GetPropertyValue("TargetFrameworkVersion"));
1920

21+
public static ImmutableArray<string> GetWebAssemblyCapabilities(this ProjectGraphNode projectNode)
22+
=> [.. projectNode.ProjectInstance.GetPropertyValue("WebAssemblyHotReloadCapabilities").Split(';').Select(static c => c.Trim()).Where(static c => c != "")];
23+
2024
public static bool IsTargetFrameworkVersionOrNewer(this ProjectGraphNode projectNode, Version minVersion)
2125
=> GetTargetFrameworkVersion(projectNode) is { } version && version >= minVersion;
2226

test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,25 @@ class AppUpdateHandler
231231
}
232232
}
233233

234-
[Fact]
235-
public async Task BlazorWasm()
234+
[Theory]
235+
[CombinatorialData]
236+
public async Task BlazorWasm(bool projectSpecifiesCapabilities)
236237
{
237-
var testAsset = TestAssets.CopyTestAsset("WatchBlazorWasm")
238+
var testAsset = TestAssets.CopyTestAsset("WatchBlazorWasm", identifier: projectSpecifiesCapabilities.ToString())
238239
.WithSource();
239240

241+
if (projectSpecifiesCapabilities)
242+
{
243+
testAsset = testAsset.WithProjectChanges(proj =>
244+
{
245+
proj.Root.Descendants()
246+
.First(e => e.Name.LocalName == "PropertyGroup")
247+
.Add(XElement.Parse("""
248+
<WebAssemblyHotReloadCapabilities>Baseline;AddMethodToExistingType</WebAssemblyHotReloadCapabilities>
249+
"""));
250+
});
251+
}
252+
240253
var port = TestOptions.GetTestPort();
241254
App.Start(testAsset, ["--urls", "http://localhost:" + port], testFlags: TestFlags.MockBrowser);
242255

@@ -256,6 +269,16 @@ public async Task BlazorWasm()
256269

257270
UpdateSourceFile(Path.Combine(testAsset.Path, "Pages", "Index.razor"), newSource);
258271
await App.AssertOutputLineStartsWith(MessageDescriptor.HotReloadSucceeded, "blazorwasm (net9.0)");
272+
273+
// check project specified capapabilities:
274+
if (projectSpecifiesCapabilities)
275+
{
276+
App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: Baseline AddMethodToExistingType.");
277+
}
278+
else
279+
{
280+
App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType.");
281+
}
259282
}
260283

261284
[Fact]

0 commit comments

Comments
 (0)