From 30653f9f967bfe58eee182a0e7594fd0ec7324ef Mon Sep 17 00:00:00 2001 From: tmat Date: Wed, 21 Jun 2023 12:08:28 -0700 Subject: [PATCH] When Hot Reload change fails to apply in the browser send full error message to the client so that it can be logged --- .../WebSocketScriptInjection.js | 22 ++++++---- .../BlazorWebAssemblyDeltaApplier.cs | 43 +++++++++++++------ 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js b/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js index 8bc073e5b944..4b13c4a52b6a 100644 --- a/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js +++ b/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js @@ -43,7 +43,8 @@ setTimeout(async function () { const payload = JSON.parse(message.data); const action = { 'UpdateStaticFile': () => updateStaticFile(payload.path), - 'BlazorHotReloadDeltav1': () => applyBlazorDeltas(payload.sharedSecret, payload.deltas), + 'BlazorHotReloadDeltav1': () => applyBlazorDeltas(payload.sharedSecret, payload.deltas, false), + 'BlazorHotReloadDeltav2': () => applyBlazorDeltas(payload.sharedSecret, payload.deltas, true), 'HotReloadDiagnosticsv1': () => displayDiagnostics(payload.diagnostics), 'BlazorRequestApplyUpdateCapabilities': getBlazorWasmApplyUpdateCapabilities, 'AspNetCoreHotReloadApplied': () => aspnetCoreHotReloadApplied() @@ -123,14 +124,14 @@ setTimeout(async function () { styleElement.parentNode.insertBefore(newElement, styleElement.nextSibling); } - function applyBlazorDeltas(serverSecret, deltas) { + function applyBlazorDeltas(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 applyFailed = false; + let applyError = undefined; if (window.Blazor?._internal?.applyHotReload) { // Only apply hot reload deltas if Blazor has been initialized. // It's possible for Blazor to start after the initial page load, so we don't consider skipping this step @@ -140,7 +141,7 @@ setTimeout(async function () { window.Blazor._internal.applyHotReload(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta) } catch (error) { console.warn(error); - applyFailed = true; + applyError = error; } }); } @@ -153,8 +154,8 @@ setTimeout(async function () { } }); - if (applyFailed) { - sendDeltaNotApplied(); + if (applyError) { + sendDeltaNotApplied(sendErrorToClient ? applyError : undefined); } else { sendDeltaApplied(); notifyHotReloadApplied(); @@ -202,8 +203,13 @@ setTimeout(async function () { connection.send(new Uint8Array([1]).buffer); } - function sendDeltaNotApplied() { - connection.send(new Uint8Array([0]).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 getSecret(serverKeyString) { diff --git a/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs index b6d672080335..2b100d61e61c 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.IO; using System.Linq; using System.Net.WebSockets; using System.Text; @@ -111,30 +112,46 @@ public override async Task Apply(DotNetWatchContext context, Immuta private async Task ReceiveApplyUpdateResult(BrowserRefreshServer browserRefresh, CancellationToken cancellationToken) { - var _receiveBuffer = new byte[1]; - var result = await browserRefresh.ReceiveAsync(_receiveBuffer, cancellationToken); - if (result is null) + var buffer = new byte[1]; + + var result = await browserRefresh.ReceiveAsync(buffer, cancellationToken); + if (result is not { MessageType: WebSocketMessageType.Binary }) { // A null result indicates no clients are connected. No deltas could have been applied in this state. _reporter.Verbose("Apply confirmation: No browser is connected"); return false; } - if (IsDeltaReceivedMessage(result.Value)) + if (result is { Count: 1, EndOfMessage: true }) { - // 1 indicates success. - return _receiveBuffer[0] == 1; + return buffer[0] == 1; } - return false; + _reporter.Verbose("Browser failed to apply the change and reported error:"); + + buffer = new byte[1024]; + var messageStream = new MemoryStream(); - bool IsDeltaReceivedMessage(ValueWebSocketReceiveResult result) + while (true) { - _reporter.Verbose($"Apply confirmation: Received {_receiveBuffer[0]} from browser in [Count: {result.Count}, MessageType: {result.MessageType}, EndOfMessage: {result.EndOfMessage}]."); - return result.Count == 1 // Should have received 1 byte on the socket for the acknowledgement - && result.MessageType is WebSocketMessageType.Binary - && result.EndOfMessage; + result = await browserRefresh.ReceiveAsync(buffer, cancellationToken); + if (result is not { MessageType: WebSocketMessageType.Binary }) + { + _reporter.Verbose("Failed to receive error message"); + break; + } + + messageStream.Write(buffer, 0, result.Value.Count); + + if (result is { EndOfMessage: true }) + { + // message and stack trace are separated by '\0' + _reporter.Verbose(Encoding.UTF8.GetString(messageStream.ToArray()).Replace("\0", Environment.NewLine)); + break; + } } + + return false; } public override void Dispose() @@ -144,7 +161,7 @@ public override void Dispose() private readonly struct UpdatePayload { - public string Type => "BlazorHotReloadDeltav1"; + public string Type => "BlazorHotReloadDeltav2"; public string? SharedSecret { get; init; } public IEnumerable Deltas { get; init; } }