Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use WebAssemblyHotReloadCapabilities project property #45055

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 31 additions & 101 deletions src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@

using System.Buffers;
using System.Collections.Immutable;
using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;

namespace Microsoft.DotNet.Watch
{
internal sealed class BlazorWebAssemblyDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, Version? targetFrameworkVersion) : SingleProcessDeltaApplier(reporter)
internal sealed class BlazorWebAssemblyDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, ProjectGraphNode project) : SingleProcessDeltaApplier(reporter)
{
private const string DefaultCapabilities60 = "Baseline";
private const string DefaultCapabilities70 = "Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes";
private const string DefaultCapabilities80 = "Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType";
private static readonly ImmutableArray<string> s_defaultCapabilities60 =
["Baseline"];

private static readonly ImmutableArray<string> s_defaultCapabilities70 =
["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "NewTypeDefinition", "ChangeCustomAttributes"];

private static readonly ImmutableArray<string> s_defaultCapabilities80 =
["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "NewTypeDefinition", "ChangeCustomAttributes",
"AddInstanceFieldToExistingType", "GenericAddMethodToExistingType", "GenericUpdateMethod", "UpdateParameters", "GenericAddFieldToExistingType"];

private static readonly ImmutableArray<string> s_defaultCapabilities90 =
s_defaultCapabilities80;

private ImmutableArray<string> _cachedCapabilities;
private readonly SemaphoreSlim _capabilityRetrievalSemaphore = new(initialCount: 1);
private int _updateId;

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

public override async Task<ImmutableArray<string>> GetApplyUpdateCapabilitiesAsync(CancellationToken cancellationToken)
public override Task<ImmutableArray<string>> GetApplyUpdateCapabilitiesAsync(CancellationToken cancellationToken)
{
var cachedCapabilities = _cachedCapabilities;
if (!cachedCapabilities.IsDefault)
{
return cachedCapabilities;
}
var capabilities = project.GetWebAssemblyCapabilities();

await _capabilityRetrievalSemaphore.WaitAsync(cancellationToken);
try
{
if (_cachedCapabilities.IsDefault)
{
_cachedCapabilities = await RetrieveAsync(cancellationToken);
}
}
finally
if (capabilities.IsEmpty)
{
_capabilityRetrievalSemaphore.Release();
}
var targetFramework = project.GetTargetFrameworkVersion();

return _cachedCapabilities;

async Task<ImmutableArray<string>> RetrieveAsync(CancellationToken cancellationToken)
{
var buffer = ArrayPool<byte>.Shared.Rent(32 * 1024);
Reporter.Verbose($"Using capabilities based on target framework: '{targetFramework}'.");

try
capabilities = targetFramework?.Major switch
{
Reporter.Verbose("Connecting to the browser.");

await browserRefreshServer.WaitForClientConnectionAsync(cancellationToken);

string capabilities;
if (browserRefreshServer.Options.TestFlags.HasFlag(TestFlags.MockBrowser))
{
// When testing return default capabilities without connecting to an actual browser.
capabilities = GetDefaultCapabilities(targetFrameworkVersion);
}
else
{
string? capabilityString = null;

await browserRefreshServer.SendAndReceiveAsync(
tmat marked this conversation as resolved.
Show resolved Hide resolved
request: _ => default(JsonGetApplyUpdateCapabilitiesRequest),
response: (value, reporter) =>
{
var str = Encoding.UTF8.GetString(value);
if (str.StartsWith('!'))
{
reporter.Verbose($"Exception while reading WASM runtime capabilities: {str[1..]}");
}
else if (str.Length == 0)
{
reporter.Verbose($"Unable to read WASM runtime capabilities");
}
else if (capabilityString == null)
{
capabilityString = str;
}
else if (capabilityString != str)
{
reporter.Verbose($"Received different capabilities from different browsers:{Environment.NewLine}'{str}'{Environment.NewLine}'{capabilityString}'");
}
},
cancellationToken);

if (capabilityString != null)
{
capabilities = capabilityString;
}
else
{
capabilities = GetDefaultCapabilities(targetFrameworkVersion);
Reporter.Verbose($"Falling back to default WASM capabilities: '{capabilities}'");
}
}

// Capabilities are expressed a space-separated string.
// e.g. https://github.com/dotnet/runtime/blob/14343bdc281102bf6fffa1ecdd920221d46761bc/src/coreclr/System.Private.CoreLib/src/System/Reflection/Metadata/AssemblyExtensions.cs#L87
return capabilities.Split(' ').ToImmutableArray();
}
catch (Exception e) when (!cancellationToken.IsCancellationRequested)
{
Reporter.Error($"Failed to read capabilities: {e.Message}");

// Do not attempt to retrieve capabilities again if it fails once, unless the operation is canceled.
return [];
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
9 => s_defaultCapabilities90,
tmat marked this conversation as resolved.
Show resolved Hide resolved
8 => s_defaultCapabilities80,
7 => s_defaultCapabilities70,
6 => s_defaultCapabilities60,
_ => [],
tmat marked this conversation as resolved.
Show resolved Hide resolved
};
}
else
{
Reporter.Verbose($"Project specifies capabilities.");
}

static string GetDefaultCapabilities(Version? targetFrameworkVersion)
=> targetFrameworkVersion?.Major switch
{
>= 8 => DefaultCapabilities80,
>= 7 => DefaultCapabilities70,
>= 6 => DefaultCapabilities60,
_ => string.Empty,
};
return Task.FromResult(capabilities);
}

public override async Task<ApplyStatus> Apply(ImmutableArray<WatchHotReloadService.Update> updates, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@


using System.Collections.Immutable;
using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;

namespace Microsoft.DotNet.Watch
{
internal sealed class BlazorWebAssemblyHostedDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, Version? targetFrameworkVersion) : DeltaApplier(reporter)
internal sealed class BlazorWebAssemblyHostedDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, ProjectGraphNode project) : DeltaApplier(reporter)
{
private readonly BlazorWebAssemblyDeltaApplier _wasmApplier = new(reporter, browserRefreshServer, targetFrameworkVersion);
private readonly BlazorWebAssemblyDeltaApplier _wasmApplier = new(reporter, browserRefreshServer, project);
private readonly DefaultDeltaApplier _hostApplier = new(reporter);

public override void Dispose()
Expand Down
9 changes: 4 additions & 5 deletions src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
_reporter.Report(MessageDescriptor.HotReloadSessionStarted);
}

private static DeltaApplier CreateDeltaApplier(HotReloadProfile profile, Version? targetFramework, BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
private static DeltaApplier CreateDeltaApplier(HotReloadProfile profile, ProjectGraphNode project, BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
=> profile switch
{
HotReloadProfile.BlazorWebAssembly => new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer!, targetFramework),
HotReloadProfile.BlazorHosted => new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer!, targetFramework),
HotReloadProfile.BlazorWebAssembly => new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer!, project),
HotReloadProfile.BlazorHosted => new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer!, project),
_ => new DefaultDeltaApplier(processReporter),
};

Expand All @@ -121,8 +121,7 @@ private static DeltaApplier CreateDeltaApplier(HotReloadProfile profile, Version
{
var projectPath = projectNode.ProjectInstance.FullPath;

var targetFramework = projectNode.GetTargetFrameworkVersion();
var deltaApplier = CreateDeltaApplier(profile, targetFramework, browserRefreshServer, processReporter);
var deltaApplier = CreateDeltaApplier(profile, projectNode, browserRefreshServer, processReporter);
var processExitedSource = new CancellationTokenSource();
var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(processExitedSource.Token, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.Build.Graph;
using Microsoft.DotNet.Cli;

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

public static ImmutableArray<string> GetWebAssemblyCapabilities(this ProjectGraphNode projectNode)
=> [.. projectNode.ProjectInstance.GetPropertyValue("WebAssemblyHotReloadCapabilities").Split(';').Select(static c => c.Trim()).Where(static c => c != "")];

public static bool IsTargetFrameworkVersionOrNewer(this ProjectGraphNode projectNode, Version minVersion)
=> GetTargetFrameworkVersion(projectNode) is { } version && version >= minVersion;

Expand Down
29 changes: 26 additions & 3 deletions test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,25 @@ class AppUpdateHandler
}
}

[Fact]
public async Task BlazorWasm()
[Theory]
[CombinatorialData]
public async Task BlazorWasm(bool projectSpecifiesCapabilities)
{
var testAsset = TestAssets.CopyTestAsset("WatchBlazorWasm")
var testAsset = TestAssets.CopyTestAsset("WatchBlazorWasm", identifier: projectSpecifiesCapabilities.ToString())
.WithSource();

if (projectSpecifiesCapabilities)
{
testAsset = testAsset.WithProjectChanges(proj =>
{
proj.Root.Descendants()
.First(e => e.Name.LocalName == "PropertyGroup")
.Add(XElement.Parse("""
<WebAssemblyHotReloadCapabilities>Baseline;AddMethodToExistingType</WebAssemblyHotReloadCapabilities>
"""));
});
}

var port = TestOptions.GetTestPort();
App.Start(testAsset, ["--urls", "http://localhost:" + port], testFlags: TestFlags.MockBrowser);

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

UpdateSourceFile(Path.Combine(testAsset.Path, "Pages", "Index.razor"), newSource);
await App.AssertOutputLineStartsWith(MessageDescriptor.HotReloadSucceeded, "blazorwasm (net9.0)");

// check project specified capapabilities:
if (projectSpecifiesCapabilities)
{
App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: Baseline AddMethodToExistingType.");
}
else
{
App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType.");
}
}

[Fact]
Expand Down
Loading