-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathWebAssemblyHotReloadClient.cs
More file actions
207 lines (172 loc) · 8.38 KB
/
WebAssemblyHotReloadClient.cs
File metadata and controls
207 lines (172 loc) · 8.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.HotReload;
internal sealed class WebAssemblyHotReloadClient(
ILogger logger,
ILogger agentLogger,
AbstractBrowserRefreshServer browserRefreshServer,
ImmutableArray<string> projectHotReloadCapabilities,
Version projectTargetFrameworkVersion,
bool suppressBrowserRequestsForTesting)
: HotReloadClient(logger, agentLogger)
{
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 readonly ImmutableArray<string> _capabilities = GetUpdateCapabilities(logger, projectHotReloadCapabilities, projectTargetFrameworkVersion);
private static ImmutableArray<string> GetUpdateCapabilities(ILogger logger, ImmutableArray<string> projectHotReloadCapabilities, Version projectTargetFrameworkVersion)
{
var capabilities = projectHotReloadCapabilities.IsEmpty
? projectTargetFrameworkVersion.Major switch
{
9 => s_defaultCapabilities90,
8 => s_defaultCapabilities80,
7 => s_defaultCapabilities70,
6 => s_defaultCapabilities60,
_ => [],
}
: projectHotReloadCapabilities;
if (capabilities is not [])
{
capabilities = AddImplicitCapabilities(capabilities);
}
var capabilitiesStr = string.Join(", ", capabilities);
if (projectHotReloadCapabilities.IsEmpty)
{
// Note that this is not possible with SDK 10+ since the WASM SDK always defines the capabilities in the project,
// but the code is shared with VS and CDK which might not use the latest SDK.
logger.Log(LogEvents.UsingCapabilitiesBasedOnTargetFrameworkVersion, projectTargetFrameworkVersion, capabilitiesStr);
}
else
{
logger.Log(LogEvents.ProjectSpecifiesCapabilities, capabilitiesStr);
}
return capabilities;
}
public override void Dispose()
{
// Do nothing.
}
public override void ConfigureLaunchEnvironment(IDictionary<string, string> environmentBuilder)
{
// the environment is configued via browser refesh server
}
public override void InitiateConnection(CancellationToken cancellationToken)
{
}
public override async Task WaitForConnectionEstablishedAsync(CancellationToken cancellationToken)
// Wait for the browser connection to be established. Currently we need the browser to be running in order to apply changes.
=> await browserRefreshServer.WaitForClientConnectionAsync(cancellationToken);
public override Task<ImmutableArray<string>> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken)
=> Task.FromResult(_capabilities);
public override async Task<Task<bool>> ApplyManagedCodeUpdatesAsync(ImmutableArray<HotReloadManagedCodeUpdate> updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken)
{
var applicableUpdates = await FilterApplicableUpdatesAsync(updates, cancellationToken);
if (applicableUpdates.Count == 0)
{
return Task.FromResult(true);
}
// When testing abstract away the browser and pretend all changes have been applied:
if (suppressBrowserRequestsForTesting)
{
return Task.FromResult(true);
}
// Make sure to send the same update to all browsers, the only difference is the shared secret.
var deltas = updates.Select(static update => new JsonDelta
{
ModuleId = update.ModuleId,
MetadataDelta = ImmutableCollectionsMarshal.AsArray(update.MetadataDelta)!,
ILDelta = ImmutableCollectionsMarshal.AsArray(update.ILDelta)!,
PdbDelta = ImmutableCollectionsMarshal.AsArray(update.PdbDelta)!,
UpdatedTypes = ImmutableCollectionsMarshal.AsArray(update.UpdatedTypes)!,
}).ToArray();
var loggingLevel = Logger.IsEnabled(LogLevel.Debug) ? ResponseLoggingLevel.Verbose : ResponseLoggingLevel.WarningsAndErrors;
// If no browser is connected we assume the changes have been applied.
// If at least one browser suceeds we consider the changes successfully applied.
// TODO:
// The refresh server should remember the deltas and apply them to browsers connected in future.
// Currently the changes are remembered on the dev server and sent over there from the browser.
// If no browser is connected the changes are not sent though.
return QueueUpdateBatch(
sendAndReceive: async batchId =>
{
var result = await browserRefreshServer.SendAndReceiveAsync(
request: sharedSecret => new JsonApplyManagedCodeUpdatesRequest
{
SharedSecret = sharedSecret,
UpdateId = batchId,
Deltas = deltas,
ResponseLoggingLevel = (int)loggingLevel
},
response: new ResponseFunc<bool>((value, logger) =>
{
var success = ReceiveUpdateResponse(value, logger);
Logger.Log(success ? LogEvents.UpdateBatchCompleted : LogEvents.UpdateBatchFailed, batchId);
return success;
}),
applyOperationCancellationToken);
return result ?? false;
},
applyOperationCancellationToken);
}
public override Task<Task<bool>> ApplyStaticAssetUpdatesAsync(ImmutableArray<HotReloadStaticAssetUpdate> updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken)
// static asset updates are handled by browser refresh server:
=> Task.FromResult(Task.FromResult(true));
private static bool ReceiveUpdateResponse(ReadOnlySpan<byte> value, ILogger logger)
{
var data = AbstractBrowserRefreshServer.DeserializeJson<JsonApplyDeltasResponse>(value);
foreach (var entry in data.Log)
{
ReportLogEntry(logger, entry.Message, (AgentMessageSeverity)entry.Severity);
}
return data.Success;
}
public override Task InitialUpdatesAppliedAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
private readonly struct JsonApplyManagedCodeUpdatesRequest
{
public string Type => "ApplyManagedCodeUpdates";
public string? SharedSecret { get; init; }
public int UpdateId { get; init; }
public JsonDelta[] Deltas { get; init; }
public int ResponseLoggingLevel { get; init; }
}
private readonly struct JsonDelta
{
public Guid ModuleId { get; init; }
public byte[] MetadataDelta { get; init; }
public byte[] ILDelta { get; init; }
public byte[] PdbDelta { get; init; }
public int[] UpdatedTypes { get; init; }
}
private readonly struct JsonApplyDeltasResponse
{
public bool Success { get; init; }
public IEnumerable<JsonLogEntry> Log { get; init; }
}
private readonly struct JsonLogEntry
{
public string Message { get; init; }
public int Severity { get; init; }
}
private readonly struct JsonGetApplyUpdateCapabilitiesRequest
{
public string Type => "GetApplyUpdateCapabilities";
}
}