Skip to content
Draft
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
178 changes: 84 additions & 94 deletions src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ private static ProjectKey GetProjectKey(ProjectGraphNode projectNode)
ProcessSpec processSpec,
EnvironmentVariablesBuilder environmentBuilder,
ProjectOptions projectOptions,
CompilationHandler compilationHandler,
CancellationToken cancellationToken)
{
BrowserRefreshServer? server;
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal sealed class BrowserRefreshServer : IAsyncDisposable, IStaticAssetChang

private readonly List<BrowserConnection> _activeConnections = [];
private readonly RSA _rsa;
private readonly CompilationHandler _compilationHandler;
private readonly IReporter _reporter;
private readonly TaskCompletionSource _terminateWebSocket;
private readonly TaskCompletionSource _browserConnected;
Expand All @@ -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);
Expand Down Expand Up @@ -184,6 +186,8 @@ private async Task WebSocketRequestAsync(HttpContext context)
_activeConnections.Add(connection);
}

// TODO: send previous updates

_browserConnected.TrySetResult();
await _terminateWebSocket.Task;
}
Expand Down
2 changes: 2 additions & 0 deletions src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public void Dispose()
Workspace?.Dispose();
}

public ImmutableList<WatchHotReloadService.Update> PreviousUpdates => _previousUpdates;

public async ValueTask TerminateNonRootProcessesAndDispose(CancellationToken cancellationToken)
{
_reporter.Verbose("Disposing remaining child processes.");
Expand Down
3 changes: 3 additions & 0 deletions src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>()
{
projectOptions.Command,
Expand Down