diff --git a/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js b/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js index aa8f1fd60f98..b53027963cd9 100644 --- a/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js +++ b/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js @@ -7,7 +7,30 @@ setTimeout(async function () { } window[scriptInjectedSentinel] = true; - // dotnet-watch browser reload script + const AgentMessageSeverity_Error = 2 + + var pendingUpdates = []; + var pendingUpdatesLog = []; + var initializedBlazor = undefined; + + window["__BlazorWebAssemblyInitializeForHotReload"] = async function (blazor) { + // apply pending updates + try { + pendingUpdates.forEach(update => pendingUpdatesLog.push(blazor.applyHotReloadDeltas(update.deltas, update.responseLoggingLevel))); + initializedBlazor = api; + updatesApplied = true; + } catch (error) { + console.error(error); + pendingUpdatesLog.push({ "message": getMessageAndStack(error), "severity": AgentMessageSeverity_Error }); + + // no updates can't be applied to this process: + pendingUpdates = undefined; + return; + } + + notifyHotReloadApplied(); + }; + // dotnet-watch browser reload script const webSocketUrls = '{{hostString}}'.split(','); const sharedSecret = await getSecret('{{ServerKey}}'); let connection; @@ -44,9 +67,18 @@ setTimeout(async function () { 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), + 'BlazorHotReloadDeltav1': () => { + validateSecret(payload.sharedSecret); + applyBlazorDeltas_legacy(payload.deltas, false); + }, + 'BlazorHotReloadDeltav2': () => { + validateSecret(payload.sharedSecret); + applyBlazorDeltas_legacy(payload.deltas, true); + }, + 'BlazorHotReloadDeltav3': () => { + validateSecret(payload.sharedSecret); + applyBlazorDeltas(payload.deltas, payload.responseLoggingLevel); + }, 'HotReloadDiagnosticsv1': () => displayDiagnostics(payload.diagnostics), 'BlazorRequestApplyUpdateCapabilities': () => getBlazorWasmApplyUpdateCapabilities(false), 'BlazorRequestApplyUpdateCapabilities2': () => getBlazorWasmApplyUpdateCapabilities(true), @@ -137,28 +169,32 @@ setTimeout(async function () { styleElement.parentNode.insertBefore(newElement, styleElement.nextSibling); } - async function applyBlazorDeltas_legacy(serverSecret, deltas, sendErrorToClient) { + function validateSecret(serverSecret) { 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.'; } + } + async function applyBlazorDeltas_legacy(deltas, sendErrorToClient) { let applyError = undefined; - - try { - applyDeltas_legacy(deltas) - } catch (error) { - console.warn(error); - applyError = error; + 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 + // to be a failure. These deltas will get applied later, when Blazor completes initialization. + deltas.forEach(d => { + try { + window.Blazor._internal.applyHotReload(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta, d.updatedTypes) + } 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 }); + await fetch('/_framework/blazor-hotreload', { method: 'post', headers: { 'content-type': 'application/json' }, body: JSON.stringify(deltas) }); } catch (error) { console.warn(error); applyError = error; @@ -172,95 +208,49 @@ setTimeout(async function () { } } - function applyDeltas_legacy(deltas) { - let apply = 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 - // to be a failure. These deltas will get applied later, when Blazor completes initialization. - if (apply) { - deltas.forEach(d => { - if (apply.length == 5) { - // WASM 8.0 - apply(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta, d.updatedTypes) - } else { - // WASM 9.0 - apply(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta) - } - }); - } - } - 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 applyBlazorDeltas(deltas, responseLoggingLevel) { + let updatesApplied = false + let success = true; + let log = []; - 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.'; + if (pendingUpdatesLog.length > 0) { + log.push(pendingUpdatesLog); + pendingUpdatesLog = []; } - const AgentMessageSeverity_Error = 2 + let wasmDeltas = deltas.map(delta => { + return { + "moduleId": delta.moduleId, + "metadataDelta": delta.metadataDelta, + "ilDelta": delta.ilDelta, + "pdbDelta": delta.pdbDelta, + "updatedTypes": delta.updatedTypes, + }; + }); - let applyError = undefined; - let log = []; - try { - let applyDeltas = window.Blazor?._internal?.applyHotReloadDeltas - if (applyDeltas) { - // 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 - // to be a failure. These deltas will get applied later, when Blazor completes initialization. - - let wasmDeltas = deltas.map(delta => { - return { - "moduleId": delta.moduleId, - "metadataDelta": delta.metadataDelta, - "ilDelta": delta.ilDelta, - "pdbDelta": delta.pdbDelta, - "updatedTypes": delta.updatedTypes, - }; - }); - - log = applyDeltas(wasmDeltas, responseLoggingLevel); - } else { - // Try invoke older WASM API: - applyDeltas_legacy(deltas) + if (initializedBlazor !== undefined) { + try { + log.push(initializedBlazor.applyHotReloadDeltas(wasmDeltas, responseLoggingLevel)); + updatesApplied = true; + } catch (error) { + console.warn(error); + log.push({ "message": getMessageAndStack(error), "severity": AgentMessageSeverity_Error }); + success = false; } - } catch (error) { - console.warn(error); - applyError = error; - log.push({ "message": getMessageAndStack(error), "severity": AgentMessageSeverity_Error }); - } - - try { - let body = JSON.stringify({ - "id": updateId, - "deltas": deltas - }); - - await fetch('/_framework/blazor-hotreload', { method: 'post', headers: { 'content-type': 'application/json' }, body: body }); - } catch (error) { - console.warn(error); - applyError = error; - log.push({ "message": getMessageAndStack(error), "severity": AgentMessageSeverity_Error }); + } else if (pendingUpdates !== undefined) { + // Blazor is not initialized, defer delta application until it is: + pendingUpdates.push({ "deltas": wasmDeltas, "responseLoggingLevel": responseLoggingLevel }) + } else { + // pending updates failed to apply, we can't apply any more updates: + success = false; } connection.send(JSON.stringify({ - "success": !applyError, + "success": success, "log": log })); - if (!applyError) { + if (updatesApplied) { notifyHotReloadApplied(); } } diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs index 38315cb09ca3..7374a0f127c1 100644 --- a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs +++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs @@ -63,6 +63,7 @@ private static ProjectKey GetProjectKey(ProjectGraphNode projectNode) ProcessSpec processSpec, EnvironmentVariablesBuilder environmentBuilder, ProjectOptions projectOptions, + CompilationHandler compilationHandler, CancellationToken cancellationToken) { BrowserRefreshServer? server; @@ -75,7 +76,7 @@ private static ProjectKey GetProjectKey(ProjectGraphNode projectNode) hasExistingServer = _servers.TryGetValue(key, out server); if (!hasExistingServer) { - server = IsServerSupported(projectNode) ? new BrowserRefreshServer(context.EnvironmentOptions, context.Reporter) : null; + server = IsServerSupported(projectNode) ? new BrowserRefreshServer(compilationHandler, context.EnvironmentOptions, context.Reporter) : null; _servers.Add(key, server); } } diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs index a8333af46546..0bfb1c32c8a2 100644 --- a/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs +++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs @@ -33,6 +33,7 @@ internal sealed class BrowserRefreshServer : IAsyncDisposable, IStaticAssetChang private readonly List _activeConnections = []; private readonly RSA _rsa; + private readonly CompilationHandler _compilationHandler; private readonly IReporter _reporter; private readonly TaskCompletionSource _terminateWebSocket; private readonly TaskCompletionSource _browserConnected; @@ -44,9 +45,10 @@ internal sealed class BrowserRefreshServer : IAsyncDisposable, IStaticAssetChang public readonly EnvironmentOptions Options; - public BrowserRefreshServer(EnvironmentOptions options, IReporter reporter) + public BrowserRefreshServer(CompilationHandler compilationHandler, EnvironmentOptions options, IReporter reporter) { _rsa = RSA.Create(2048); + _compilationHandler = compilationHandler; Options = options; _reporter = reporter; _terminateWebSocket = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -184,6 +186,8 @@ private async Task WebSocketRequestAsync(HttpContext context) _activeConnections.Add(connection); } + // TODO: send previous updates + _browserConnected.TrySetResult(); await _terminateWebSocket.Task; } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs index 15c4ea831d9b..f5d39a5e1571 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs @@ -56,6 +56,8 @@ public void Dispose() Workspace?.Dispose(); } + public ImmutableList PreviousUpdates => _previousUpdates; + public async ValueTask TerminateNonRootProcessesAndDispose(CancellationToken cancellationToken) { _reporter.Verbose("Disposing remaining child processes."); diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs b/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs index c68073001705..7023182adabc 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs @@ -99,6 +99,9 @@ public EnvironmentOptions EnvironmentOptions var browserRefreshServer = await browserConnector.GetOrCreateBrowserRefreshServerAsync(projectNode, processSpec, environmentBuilder, projectOptions, cancellationToken); + //TODO + environmentBuilder.ConfigureProcess(processSpec) + var arguments = new List() { projectOptions.Command,