From f4122aeaec07b730e739dff822fb6fd3c6c8f6c2 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 17 Oct 2025 14:03:41 -0700 Subject: [PATCH] Temporarily revert "Clean up browser communication protocol (#51226)" so that we can get a source package that's matching 10.0.1xx This reverts commit 43f8f05f3dc65cc2e21f8be91bfd79ced1992dd4. --- .../Web/AbstractBrowserRefreshServer.cs | 40 +++-- .../Web/WebAssemblyHotReloadClient.cs | 8 +- .../WebSocketScriptInjection.js | 139 +++++++++++------- .../Browser/BrowserTests.cs | 12 +- 4 files changed, 115 insertions(+), 84 deletions(-) diff --git a/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs b/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs index d39ea6c0f519..d3608901433e 100644 --- a/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs +++ b/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs @@ -29,6 +29,9 @@ internal abstract class AbstractBrowserRefreshServer(string middlewareAssemblyPa { public const string ServerLogComponentName = "BrowserRefreshServer"; + private static readonly ReadOnlyMemory s_reloadMessage = Encoding.UTF8.GetBytes("Reload"); + private static readonly ReadOnlyMemory s_waitMessage = Encoding.UTF8.GetBytes("Wait"); + private static readonly ReadOnlyMemory s_pingMessage = Encoding.UTF8.GetBytes("""{ "type" : "Ping" }"""); private static readonly JsonSerializerOptions s_jsonSerializerOptions = new(JsonSerializerDefaults.Web); private readonly List _activeConnections = []; @@ -230,15 +233,19 @@ public ValueTask SendJsonMessageAsync(TValue value, CancellationToken ca public ValueTask SendReloadMessageAsync(CancellationToken cancellationToken) { logger.Log(LogEvents.ReloadingBrowser); - return SendAsync(JsonReloadRequest.Message, cancellationToken); + return SendAsync(s_reloadMessage, cancellationToken); } public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken) { logger.Log(LogEvents.SendingWaitMessage); - return SendAsync(JsonWaitRequest.Message, cancellationToken); + return SendAsync(s_waitMessage, cancellationToken); } + // obsolete: to be removed + public ValueTask SendPingMessageAsync(CancellationToken cancellationToken) + => SendAsync(s_pingMessage, cancellationToken); + private ValueTask SendAsync(ReadOnlyMemory messageBytes, CancellationToken cancellationToken) => SendAndReceiveAsync(request: _ => messageBytes, response: null, cancellationToken); @@ -286,13 +293,13 @@ public async ValueTask SendAndReceiveAsync( public ValueTask RefreshBrowserAsync(CancellationToken cancellationToken) { logger.Log(LogEvents.RefreshingBrowser); - return SendAsync(JsonRefreshBrowserRequest.Message, cancellationToken); + return SendJsonMessageAsync(new AspNetCoreHotReloadApplied(), cancellationToken); } public ValueTask ReportCompilationErrorsInBrowserAsync(ImmutableArray compilationErrors, CancellationToken cancellationToken) { logger.Log(LogEvents.UpdatingDiagnostics); - return SendJsonMessageAsync(new JsonReportDiagnosticsRequest { Diagnostics = compilationErrors }, cancellationToken); + return SendJsonMessageAsync(new HotReloadDiagnostics { Diagnostics = compilationErrors }, cancellationToken); } public async ValueTask UpdateStaticAssetsAsync(IEnumerable relativeUrls, CancellationToken cancellationToken) @@ -301,37 +308,24 @@ public async ValueTask UpdateStaticAssetsAsync(IEnumerable relativeUrls, foreach (var relativeUrl in relativeUrls) { logger.Log(LogEvents.SendingStaticAssetUpdateRequest, relativeUrl); - var message = JsonSerializer.SerializeToUtf8Bytes(new JasonUpdateStaticFileRequest { Path = relativeUrl }, s_jsonSerializerOptions); + var message = JsonSerializer.SerializeToUtf8Bytes(new UpdateStaticFileMessage { Path = relativeUrl }, s_jsonSerializerOptions); await SendAsync(message, cancellationToken); } } - private readonly struct JsonWaitRequest - { - public string Type => "Wait"; - public static readonly ReadOnlyMemory Message = JsonSerializer.SerializeToUtf8Bytes(new JsonWaitRequest(), s_jsonSerializerOptions); - } - - private readonly struct JsonReloadRequest - { - public string Type => "Reload"; - public static readonly ReadOnlyMemory Message = JsonSerializer.SerializeToUtf8Bytes(new JsonReloadRequest(), s_jsonSerializerOptions); - } - - private readonly struct JsonRefreshBrowserRequest + private readonly struct AspNetCoreHotReloadApplied { - public string Type => "RefreshBrowser"; - public static readonly ReadOnlyMemory Message = JsonSerializer.SerializeToUtf8Bytes(new JsonRefreshBrowserRequest(), s_jsonSerializerOptions); + public string Type => "AspNetCoreHotReloadApplied"; } - private readonly struct JsonReportDiagnosticsRequest + private readonly struct HotReloadDiagnostics { - public string Type => "ReportDiagnostics"; + public string Type => "HotReloadDiagnosticsv1"; public IEnumerable Diagnostics { get; init; } } - private readonly struct JasonUpdateStaticFileRequest + private readonly struct UpdateStaticFileMessage { public string Type => "UpdateStaticFile"; public string Path { get; init; } diff --git a/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs b/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs index f47d6eb3a5ba..2a45bfb07370 100644 --- a/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs @@ -133,7 +133,7 @@ public override async Task ApplyManagedCodeUpdatesAsync(ImmutableAr var anyFailure = false; await browserRefreshServer.SendAndReceiveAsync( - request: sharedSecret => new JsonApplyManagedCodeUpdatesRequest + request: sharedSecret => new JsonApplyHotReloadDeltasRequest { SharedSecret = sharedSecret, UpdateId = batchId, @@ -178,9 +178,9 @@ private static bool ReceiveUpdateResponseAsync(ReadOnlySpan value, ILogger public override Task InitialUpdatesAppliedAsync(CancellationToken cancellationToken) => Task.CompletedTask; - private readonly struct JsonApplyManagedCodeUpdatesRequest + private readonly struct JsonApplyHotReloadDeltasRequest { - public string Type => "ApplyManagedCodeUpdates"; + public string Type => "BlazorHotReloadDeltav3"; public string? SharedSecret { get; init; } public int UpdateId { get; init; } @@ -211,7 +211,7 @@ private readonly struct JsonLogEntry private readonly struct JsonGetApplyUpdateCapabilitiesRequest { - public string Type => "GetApplyUpdateCapabilities"; + public string Type => "BlazorRequestApplyUpdateCapabilities2"; } } } diff --git a/src/BuiltInTools/Web.Middleware/WebSocketScriptInjection.js b/src/BuiltInTools/Web.Middleware/WebSocketScriptInjection.js index 44d6ae3250ad..aa8f1fd60f98 100644 --- a/src/BuiltInTools/Web.Middleware/WebSocketScriptInjection.js +++ b/src/BuiltInTools/Web.Middleware/WebSocketScriptInjection.js @@ -26,22 +26,38 @@ setTimeout(async function () { let waiting = false; - connection.onmessage = function (message) { - const payload = JSON.parse(message.data); - const action = { - 'Reload': () => reload(), - 'Wait': () => wait(), - 'UpdateStaticFile': () => updateStaticFile(payload.path), - 'ApplyManagedCodeUpdates': () => applyManagedCodeUpdates(payload.sharedSecret, payload.updateId, payload.deltas, payload.responseLoggingLevel), - 'ReportDiagnostics': () => reportDiagnostics(payload.diagnostics), - 'GetApplyUpdateCapabilities': () => getApplyUpdateCapabilities(), - 'RefreshBrowser': () => refreshBrowser() - }; - - if (payload.type && action.hasOwnProperty(payload.type)) { - action[payload.type](); + connection.onmessage = function (message) { + if (message.data === 'Reload') { + console.debug('Server is ready. Reloading...'); + location.reload(); + } else if (message.data === 'Wait') { + if (waiting) { + return; + } + waiting = true; + console.debug('File changes detected. Waiting for application to rebuild.'); + const glyphs = ['☱', '☲', '☴']; + const title = document.title; + let i = 0; + setInterval(function () { document.title = glyphs[i++ % glyphs.length] + ' ' + title; }, 240); } else { - console.error('Unknown payload:', message.data); + const payload = JSON.parse(message.data); + const action = { + 'UpdateStaticFile': () => updateStaticFile(payload.path), + 'BlazorHotReloadDeltav1': () => applyBlazorDeltas_legacy(payload.sharedSecret, payload.deltas, false), + 'BlazorHotReloadDeltav2': () => applyBlazorDeltas_legacy(payload.sharedSecret, payload.deltas, true), + 'BlazorHotReloadDeltav3': () => applyBlazorDeltas(payload.sharedSecret, payload.updateId, payload.deltas, payload.responseLoggingLevel), + 'HotReloadDiagnosticsv1': () => displayDiagnostics(payload.diagnostics), + 'BlazorRequestApplyUpdateCapabilities': () => getBlazorWasmApplyUpdateCapabilities(false), + 'BlazorRequestApplyUpdateCapabilities2': () => getBlazorWasmApplyUpdateCapabilities(true), + 'AspNetCoreHotReloadApplied': () => aspnetCoreHotReloadApplied() + }; + + if (payload.type && action.hasOwnProperty(payload.type)) { + action[payload.type](); + } else { + console.error('Unknown payload:', message.data); + } } } @@ -90,12 +106,12 @@ setTimeout(async function () { return messageAndStack } - function getApplyUpdateCapabilities() { + function getBlazorWasmApplyUpdateCapabilities(sendErrorToClient) { let applyUpdateCapabilities; try { applyUpdateCapabilities = window.Blazor._internal.getApplyUpdateCapabilities(); } catch (error) { - applyUpdateCapabilities = "!" + getMessageAndStack(error); + applyUpdateCapabilities = sendErrorToClient ? "!" + getMessageAndStack(error) : ''; } connection.send(applyUpdateCapabilities); } @@ -121,6 +137,41 @@ setTimeout(async function () { styleElement.parentNode.insertBefore(newElement, styleElement.nextSibling); } + async function applyBlazorDeltas_legacy(serverSecret, deltas, sendErrorToClient) { + if (sharedSecret && (serverSecret != sharedSecret.encodedSharedSecret)) { + // Validate the shared secret if it was specified. It might be unspecified in older versions of VS + // that do not support this feature as yet. + throw 'Unable to validate the server. Rejecting apply-update payload.'; + } + + let applyError = undefined; + + try { + applyDeltas_legacy(deltas) + } catch (error) { + console.warn(error); + applyError = error; + } + + const body = JSON.stringify({ + id: deltas[0].sequenceId, + deltas: deltas + }); + try { + await fetch('/_framework/blazor-hotreload', { method: 'post', headers: { 'content-type': 'application/json' }, body: body }); + } catch (error) { + console.warn(error); + applyError = error; + } + + if (applyError) { + sendDeltaNotApplied(sendErrorToClient ? applyError : undefined); + } else { + sendDeltaApplied(); + notifyHotReloadApplied(); + } + } + function applyDeltas_legacy(deltas) { let apply = window.Blazor?._internal?.applyHotReload @@ -139,16 +190,26 @@ setTimeout(async function () { }); } } + function sendDeltaApplied() { + connection.send(new Uint8Array([1]).buffer); + } + + function sendDeltaNotApplied(error) { + if (error) { + let encoder = new TextEncoder() + connection.send(encoder.encode("\0" + error.message + "\0" + error.stack)); + } else { + connection.send(new Uint8Array([0]).buffer); + } + } - async function applyManagedCodeUpdates(serverSecret, updateId, deltas, responseLoggingLevel) { + async function applyBlazorDeltas(serverSecret, updateId, deltas, responseLoggingLevel) { if (sharedSecret && (serverSecret != sharedSecret.encodedSharedSecret)) { // Validate the shared secret if it was specified. It might be unspecified in older versions of VS // that do not support this feature as yet. throw 'Unable to validate the server. Rejecting apply-update payload.'; } - console.debug('Applying managed code updates.'); - const AgentMessageSeverity_Error = 2 let applyError = undefined; @@ -200,13 +261,11 @@ setTimeout(async function () { })); if (!applyError) { - displayChangesAppliedToast(); + notifyHotReloadApplied(); } } - function reportDiagnostics(diagnostics) { - console.debug('Reporting Hot Reload diagnostics.'); - + function displayDiagnostics(diagnostics) { document.querySelectorAll('#dotnet-compile-error').forEach(el => el.remove()); const el = document.body.appendChild(document.createElement('div')); el.id = 'dotnet-compile-error'; @@ -221,7 +280,7 @@ setTimeout(async function () { }); } - function displayChangesAppliedToast() { + function notifyHotReloadApplied() { document.querySelectorAll('#dotnet-compile-error').forEach(el => el.remove()); if (document.querySelector('#dotnet-hotreload-toast')) { return; @@ -239,7 +298,7 @@ setTimeout(async function () { setTimeout(() => el.remove(), 2000); } - function refreshBrowser() { + function aspnetCoreHotReloadApplied() { if (window.Blazor) { window[hotReloadActiveKey] = true; // hotReloadApplied triggers an enhanced navigation to @@ -247,39 +306,17 @@ setTimeout(async function () { // Blazor SSR. if (window.Blazor?._internal?.hotReloadApplied) { - console.debug('Refreshing browser: WASM.'); Blazor._internal.hotReloadApplied(); } else { - console.debug('Refreshing browser.'); - displayChangesAppliedToast(); + notifyHotReloadApplied(); } } else { - console.debug('Refreshing browser: Reloading.'); location.reload(); } } - function reload() { - console.debug('Reloading.'); - location.reload(); - } - - function wait() { - console.debug('Waiting for application to rebuild.'); - - if (waiting) { - return; - } - - waiting = true; - const glyphs = ['☱', '☲', '☴']; - const title = document.title; - let i = 0; - setInterval(function () { document.title = glyphs[i++ % glyphs.length] + ' ' + title; }, 240); - } - async function getSecret(serverKeyString) { if (!serverKeyString || !window.crypto || !window.crypto.subtle) { return null; @@ -345,8 +382,8 @@ setTimeout(async function () { webSocket.addEventListener('close', onClose); if (window.Blazor?.removeEventListener && window.Blazor?.addEventListener) { - webSocket.addEventListener('close', () => window.Blazor?.removeEventListener('enhancedload', displayChangesAppliedToast)); - window.Blazor?.addEventListener('enhancedload', displayChangesAppliedToast); + webSocket.addEventListener('close', () => window.Blazor?.removeEventListener('enhancedload', notifyHotReloadApplied)); + window.Blazor?.addEventListener('enhancedload', notifyHotReloadApplied); } }); } diff --git a/test/dotnet-watch.Tests/Browser/BrowserTests.cs b/test/dotnet-watch.Tests/Browser/BrowserTests.cs index 17e21166bf3f..d1c302337036 100644 --- a/test/dotnet-watch.Tests/Browser/BrowserTests.cs +++ b/test/dotnet-watch.Tests/Browser/BrowserTests.cs @@ -65,7 +65,7 @@ public async Task BrowserDiagnostics() await App.WaitForOutputLineContaining("Do you want to restart your app?"); await App.WaitUntilOutputContains($$""" - 🧪 Received: {"type":"ReportDiagnostics","diagnostics":[{{jsonErrorMessage}}]} + 🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":[{{jsonErrorMessage}}]} """); // auto restart next time: @@ -76,7 +76,7 @@ await App.WaitUntilOutputContains($$""" // browser page was reloaded after the app restarted: await App.WaitUntilOutputContains(""" - 🧪 Received: {"type":"Reload"} + 🧪 Received: Reload """); // no other browser message sent: @@ -93,14 +93,14 @@ await App.WaitUntilOutputContains(""" await App.WaitForOutputLineContaining("[auto-restart] " + errorMessage); await App.WaitUntilOutputContains($$""" - 🧪 Received: {"type":"ReportDiagnostics","diagnostics":["Restarting application to apply changes ..."]} + 🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":["Restarting application to apply changes ..."]} """); await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); // browser page was reloaded after the app restarted: await App.WaitUntilOutputContains(""" - 🧪 Received: {"type":"Reload"} + 🧪 Received: Reload """); // no other browser message sent: @@ -114,11 +114,11 @@ await App.WaitUntilOutputContains(""" await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded); await App.WaitUntilOutputContains($$""" - 🧪 Received: {"type":"ReportDiagnostics","diagnostics":[]} + 🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":[]} """); await App.WaitUntilOutputContains($$""" - 🧪 Received: {"type":"RefreshBrowser"} + 🧪 Received: {"type":"AspNetCoreHotReloadApplied"} """); // no other browser message sent: