Skip to content

Commit 4177291

Browse files
authored
Implement partial solution update support and fix bugs (#49471)
1 parent dff2ba5 commit 4177291

File tree

18 files changed

+230
-89
lines changed

18 files changed

+230
-89
lines changed

src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ namespace Microsoft.DotNet.Watch
1414
internal sealed class CompilationHandler : IDisposable
1515
{
1616
public readonly IncrementalMSBuildWorkspace Workspace;
17-
public readonly EnvironmentOptions EnvironmentOptions;
1817
private readonly IReporter _reporter;
1918
private readonly WatchHotReloadService _hotReloadService;
2019
private readonly ProcessRunner _processRunner;
@@ -39,11 +38,15 @@ internal sealed class CompilationHandler : IDisposable
3938

4039
private bool _isDisposed;
4140

42-
public CompilationHandler(IReporter reporter, ProcessRunner processRunner, EnvironmentOptions environmentOptions)
41+
static CompilationHandler()
42+
{
43+
WatchHotReloadService.RequireCommit = true;
44+
}
45+
46+
public CompilationHandler(IReporter reporter, ProcessRunner processRunner)
4347
{
4448
_reporter = reporter;
4549
_processRunner = processRunner;
46-
EnvironmentOptions = environmentOptions;
4750
Workspace = new IncrementalMSBuildWorkspace(reporter);
4851
_hotReloadService = new WatchHotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities()));
4952
}
@@ -68,7 +71,7 @@ public async ValueTask TerminateNonRootProcessesAndDispose(CancellationToken can
6871
Dispose();
6972
}
7073

71-
public void DiscardProjectBaselines(ImmutableDictionary<ProjectId, string> projectsToBeRebuilt, CancellationToken cancellationToken)
74+
private void DiscardPreviousUpdates(ImmutableArray<ProjectId> projectsToBeRebuilt)
7275
{
7376
// Remove previous updates to all modules that were affected by rude edits.
7477
// All running projects that statically reference these modules have been terminated.
@@ -78,17 +81,9 @@ public void DiscardProjectBaselines(ImmutableDictionary<ProjectId, string> proje
7881

7982
lock (_runningProjectsAndUpdatesGuard)
8083
{
81-
_previousUpdates = _previousUpdates.RemoveAll(update => projectsToBeRebuilt.ContainsKey(update.ProjectId));
84+
_previousUpdates = _previousUpdates.RemoveAll(update => projectsToBeRebuilt.Contains(update.ProjectId));
8285
}
83-
84-
_hotReloadService.UpdateBaselines(Workspace.CurrentSolution, projectsToBeRebuilt.Keys.ToImmutableArray());
8586
}
86-
87-
public void UpdateProjectBaselines(ImmutableDictionary<ProjectId, string> projectsToBeRebuilt, CancellationToken cancellationToken)
88-
{
89-
_hotReloadService.UpdateBaselines(Workspace.CurrentSolution, projectsToBeRebuilt.Keys.ToImmutableArray());
90-
}
91-
9287
public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
9388
{
9489
_reporter.Report(MessageDescriptor.HotReloadSessionStarting);
@@ -245,7 +240,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
245240

246241
public async ValueTask<(ImmutableDictionary<ProjectId, string> projectsToRebuild, ImmutableArray<RunningProject> terminatedProjects)> HandleManagedCodeChangesAsync(
247242
bool autoRestart,
248-
Func<IEnumerable<string>, CancellationToken, Task> restartPrompt,
243+
Func<IEnumerable<string>, CancellationToken, Task<bool>> restartPrompt,
249244
CancellationToken cancellationToken)
250245
{
251246
var currentSolution = Workspace.CurrentSolution;
@@ -267,10 +262,28 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
267262
{
268263
// If Hot Reload is blocked (due to compilation error) we ignore the current
269264
// changes and await the next file change.
270-
return (ImmutableDictionary<ProjectId, string>.Empty, []);
265+
266+
// Note: CommitUpdate/DiscardUpdate is not expected to be called.
267+
return ([], []);
268+
}
269+
270+
var projectsToPromptForRestart =
271+
(from projectId in updates.ProjectsToRestart.Keys
272+
where !runningProjectInfos[projectId].RestartWhenChangesHaveNoEffect // equivallent to auto-restart
273+
select currentSolution.GetProject(projectId)!.Name).ToList();
274+
275+
if (projectsToPromptForRestart.Any() &&
276+
!await restartPrompt.Invoke(projectsToPromptForRestart, cancellationToken))
277+
{
278+
_hotReloadService.DiscardUpdate();
279+
280+
_reporter.Output("Hot reload suspended. To continue hot reload, press \"Ctrl + R\".", emoji: "🔥");
281+
await Task.Delay(-1, cancellationToken);
282+
283+
return ([], []);
271284
}
272285

273-
if (updates.ProjectUpdates.Any())
286+
if (!updates.ProjectUpdates.IsEmpty)
274287
{
275288
ImmutableDictionary<string, ImmutableArray<RunningProject>> projectsToUpdate;
276289
lock (_runningProjectsAndUpdatesGuard)
@@ -308,27 +321,18 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT
308321
}, cancellationToken);
309322
}
310323

311-
if (updates.ProjectsToRestart.IsEmpty)
312-
{
313-
return (ImmutableDictionary<ProjectId, string>.Empty, []);
314-
}
315-
316-
// Terminate projects that need restarting.
324+
// Note: Releases locked project baseline readers, so we can rebuild any projects that need rebuilding.
325+
_hotReloadService.CommitUpdate();
317326

318-
var projectsToPromptForRestart =
319-
(from projectId in updates.ProjectsToRestart.Keys
320-
where !runningProjectInfos[projectId].RestartWhenChangesHaveNoEffect // equivallent to auto-restart
321-
select currentSolution.GetProject(projectId)!.Name).ToList();
327+
DiscardPreviousUpdates(updates.ProjectsToRebuild);
322328

323-
if (projectsToPromptForRestart is not [])
324-
{
325-
await restartPrompt.Invoke(projectsToPromptForRestart, cancellationToken);
326-
}
329+
var projectsToRebuild = updates.ProjectsToRebuild.ToImmutableDictionary(keySelector: id => id, elementSelector: id => currentSolution.GetProject(id)!.FilePath!);
327330

328331
// Terminate all tracked processes that need to be restarted,
329332
// except for the root process, which will terminate later on.
330-
var terminatedProjects = await TerminateNonRootProcessesAsync(updates.ProjectsToRestart.Select(e => currentSolution.GetProject(e.Key)!.FilePath!), cancellationToken);
331-
var projectsToRebuild = updates.ProjectsToRebuild.ToImmutableDictionary(keySelector: id => id, elementSelector: id => currentSolution.GetProject(id)!.FilePath!);
333+
var terminatedProjects = updates.ProjectsToRestart.IsEmpty
334+
? []
335+
: await TerminateNonRootProcessesAsync(updates.ProjectsToRestart.Select(e => currentSolution.GetProject(e.Key)!.FilePath!), cancellationToken);
332336

333337
return (projectsToRebuild, terminatedProjects);
334338
}
@@ -398,6 +402,14 @@ void ReportCompilationDiagnostics(DiagnosticSeverity severity)
398402
continue;
399403
}
400404

405+
// TODO: https://github.com/dotnet/roslyn/pull/79018
406+
// shouldn't be included in compilation diagnostics
407+
if (diagnostic.Id == "ENC0118")
408+
{
409+
// warning ENC0118: Changing 'top-level code' might not have any effect until the application is restarted
410+
continue;
411+
}
412+
401413
if (diagnostic.DefaultSeverity != severity)
402414
{
403415
continue;

src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,16 @@ public async ValueTask UpdateFileContentAsync(IEnumerable<ChangedFile> changedFi
145145
var project = updatedSolution.GetProject(documentId.ProjectId);
146146
Debug.Assert(project?.FilePath != null);
147147

148-
var sourceText = await GetSourceTextAsync(changedFile.FilePath, cancellationToken);
148+
var oldText = await textDocument.GetTextAsync(cancellationToken);
149+
Debug.Assert(oldText.Encoding != null);
150+
151+
var newText = await GetSourceTextAsync(changedFile.FilePath, oldText.Encoding, oldText.ChecksumAlgorithm, cancellationToken);
149152

150153
updatedSolution = textDocument switch
151154
{
152-
Document document => document.WithText(sourceText).Project.Solution,
153-
AdditionalDocument ad => updatedSolution.WithAdditionalDocumentText(textDocument.Id, sourceText, PreservationMode.PreserveValue),
154-
AnalyzerConfigDocument acd => updatedSolution.WithAnalyzerConfigDocumentText(textDocument.Id, sourceText, PreservationMode.PreserveValue),
155+
Document document => document.WithText(newText).Project.Solution,
156+
AdditionalDocument ad => updatedSolution.WithAdditionalDocumentText(textDocument.Id, newText, PreservationMode.PreserveValue),
157+
AnalyzerConfigDocument acd => updatedSolution.WithAnalyzerConfigDocumentText(textDocument.Id, newText, PreservationMode.PreserveValue),
155158
_ => throw new InvalidOperationException()
156159
};
157160
}
@@ -164,11 +167,11 @@ public async ValueTask UpdateFileContentAsync(IEnumerable<ChangedFile> changedFi
164167

165168
private static Solution RemoveDocuments(Solution solution, IEnumerable<DocumentId> ids)
166169
=> solution
167-
.RemoveDocuments(ids.Where(id => solution.GetDocument(id) != null).ToImmutableArray())
168-
.RemoveAdditionalDocuments(ids.Where(id => solution.GetAdditionalDocument(id) != null).ToImmutableArray())
169-
.RemoveAnalyzerConfigDocuments(ids.Where(id => solution.GetAnalyzerConfigDocument(id) != null).ToImmutableArray());
170+
.RemoveDocuments([.. ids.Where(id => solution.GetDocument(id) != null)])
171+
.RemoveAdditionalDocuments([.. ids.Where(id => solution.GetAdditionalDocument(id) != null)])
172+
.RemoveAnalyzerConfigDocuments([.. ids.Where(id => solution.GetAnalyzerConfigDocument(id) != null)]);
170173

171-
private static async ValueTask<SourceText> GetSourceTextAsync(string filePath, CancellationToken cancellationToken)
174+
private static async ValueTask<SourceText> GetSourceTextAsync(string filePath, Encoding encoding, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken)
172175
{
173176
var zeroLengthRetryPerformed = false;
174177
for (var attemptIndex = 0; attemptIndex < 6; attemptIndex++)
@@ -180,7 +183,7 @@ private static async ValueTask<SourceText> GetSourceTextAsync(string filePath, C
180183
SourceText sourceText;
181184
using (var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
182185
{
183-
sourceText = SourceText.From(stream, Encoding.UTF8);
186+
sourceText = SourceText.From(stream, encoding, checksumAlgorithm);
184187
}
185188

186189
if (!zeroLengthRetryPerformed && sourceText.Length == 0)
@@ -195,7 +198,7 @@ private static async ValueTask<SourceText> GetSourceTextAsync(string filePath, C
195198
await Task.Delay(20, cancellationToken);
196199

197200
using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
198-
sourceText = SourceText.From(stream, Encoding.UTF8);
201+
sourceText = SourceText.From(stream, encoding, checksumAlgorithm);
199202
}
200203

201204
return sourceText;

src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
100100
}
101101

102102
var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, Context.Reporter);
103-
compilationHandler = new CompilationHandler(Context.Reporter, Context.ProcessRunner, Context.EnvironmentOptions);
103+
compilationHandler = new CompilationHandler(Context.Reporter, Context.ProcessRunner);
104104
var scopedCssFileHandler = new ScopedCssFileHandler(Context.Reporter, projectMap, browserConnector);
105105
var projectLauncher = new ProjectLauncher(Context, projectMap, browserConnector, compilationHandler, iteration);
106106
var outputDirectories = GetProjectOutputDirectories(evaluationResult.ProjectGraph);
@@ -291,28 +291,23 @@ void FileChangedCallback(ChangedPath change)
291291
question = "Do you want to restart these projects?";
292292
}
293293

294-
if (!await _rudeEditRestartPrompt.WaitForRestartConfirmationAsync(question, cancellationToken))
295-
{
296-
Context.Reporter.Output("Hot reload suspended. To continue hot reload, press \"Ctrl + R\".", emoji: "🔥");
297-
await Task.Delay(-1, cancellationToken);
298-
}
294+
return await _rudeEditRestartPrompt.WaitForRestartConfirmationAsync(question, cancellationToken);
299295
}
300-
else
301-
{
302-
Context.Reporter.Verbose("Restarting without prompt since dotnet-watch is running in non-interactive mode.");
303296

304-
foreach (var projectName in projectNames)
305-
{
306-
Context.Reporter.Verbose($" Project to restart: '{projectName}'");
307-
}
297+
Context.Reporter.Verbose("Restarting without prompt since dotnet-watch is running in non-interactive mode.");
298+
299+
foreach (var projectName in projectNames)
300+
{
301+
Context.Reporter.Verbose($" Project to restart: '{projectName}'");
308302
}
303+
304+
return true;
309305
},
310306
iterationCancellationToken);
311307

312308
HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
313309

314310
stopwatch.Stop();
315-
Context.Reporter.Report(MessageDescriptor.HotReloadChangeHandled, stopwatch.ElapsedMilliseconds);
316311

317312
HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.Main);
318313

@@ -324,11 +319,8 @@ void FileChangedCallback(ChangedPath change)
324319
break;
325320
}
326321

327-
if (projectsToRebuild.Count > 0)
322+
if (!projectsToRebuild.IsEmpty)
328323
{
329-
// Discard baselines before build.
330-
compilationHandler.DiscardProjectBaselines(projectsToRebuild, iterationCancellationToken);
331-
332324
while (true)
333325
{
334326
iterationCancellationToken.ThrowIfCancellationRequested();
@@ -367,13 +359,10 @@ void FileChangedCallback(ChangedPath change)
367359
// Apply them to the workspace.
368360
_ = await CaptureChangedFilesSnapshot(projectsToRebuild);
369361

370-
// Update project baselines to reflect changes to the restarted projects.
371-
compilationHandler.UpdateProjectBaselines(projectsToRebuild, iterationCancellationToken);
372-
373362
Context.Reporter.Report(MessageDescriptor.ProjectsRebuilt, projectsToRebuild.Count);
374363
}
375364

376-
if (projectsToRestart is not [])
365+
if (!projectsToRestart.IsEmpty)
377366
{
378367
await Task.WhenAll(
379368
projectsToRestart.Select(async runningProject =>
@@ -398,6 +387,8 @@ await Task.WhenAll(
398387
Context.Reporter.Report(MessageDescriptor.ProjectsRestarted, projectsToRestart.Length);
399388
}
400389

390+
Context.Reporter.Report(MessageDescriptor.HotReloadChangeHandled, stopwatch.ElapsedMilliseconds);
391+
401392
async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDictionary<ProjectId, string>? rebuiltProjects)
402393
{
403394
var changedPaths = Interlocked.Exchange(ref changedFilesAccumulator, []);

src/BuiltInTools/dotnet-watch/Properties/launchSettings.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
"profiles": {
33
"dotnet-watch": {
44
"commandName": "Project",
5-
"commandLineArgs": "--verbose /bl:DotnetRun.binlog",
6-
"workingDirectory": "$(RepoRoot)src\\Assets\\TestProjects\\BlazorWasmWithLibrary\\blazorwasm",
5+
"commandLineArgs": "--verbose /bl:DotnetRun.binlog -lp http --non-interactive",
6+
"workingDirectory": "C:\\sdk1\\artifacts\\tmp\\Debug\\Aspire_BuildE---04F22750\\WatchAspire.AppHost",
77
"environmentVariables": {
88
"DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)",
99
"DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000",
1010
"DCP_IDE_NOTIFICATION_TIMEOUT_SECONDS": "100000",
1111
"DCP_IDE_NOTIFICATION_KEEPALIVE_SECONDS": "100000",
12-
"DOTNET_WATCH_PROCESS_CLEANUP_TIMEOUT_MS": "0"
12+
"DOTNET_WATCH_PROCESS_CLEANUP_TIMEOUT_MS": "0",
13+
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "1"
1314
}
1415
}
1516
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
var builder = DistributedApplication.CreateBuilder(args);
22

3-
builder.AddProject<Projects.WatchAspire_ApiService>("apiservice");
3+
var apiService = builder.AddProject<Projects.WatchAspire_ApiService>("apiservice");
4+
5+
builder.AddProject<Projects.WatchAspire_Web>("webfrontend")
6+
.WithExternalHttpEndpoints()
7+
.WithReference(apiService)
8+
.WaitFor(apiService);
49

510
builder.Build().Run();

test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
<ItemGroup>
1212
<ProjectReference Include="..\WatchAspire.ApiService\WatchAspire.ApiService.csproj" Watch="true" />
13+
<ProjectReference Include="..\WatchAspire.Web\WatchAspire.Web.csproj" Watch="true" />
1314
<ProjectReference Include="..\WatchAspire.Wasm\WatchAspire.Wasm.csproj" Watch="true" />
1415
</ItemGroup>
1516

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@code {
2+
[Parameter]
3+
public IEnumerable<string> Photos { get; set; } = [];
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Microsoft.AspNetCore.Http.HttpResults;
2+
using Microsoft.Extensions.Hosting;
3+
using WatchAspire.Web.Components;
4+
5+
var builder = WebApplication.CreateBuilder(args);
6+
7+
/* top-level placeholder */
8+
9+
var app = builder.Build();
10+
app.MapGet("/", () => "Hello world!");
11+
app.Run();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "http://localhost:5200",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development"
11+
}
12+
},
13+
"https": {
14+
"commandName": "Project",
15+
"dotnetRunMessages": true,
16+
"launchBrowser": true,
17+
"applicationUrl": "https://localhost:7125;http://localhost:5200",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development"
20+
}
21+
}
22+
}
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="..\WatchAspire.ServiceDefaults\WatchAspire.ServiceDefaults.csproj" />
11+
</ItemGroup>
12+
13+
</Project>

0 commit comments

Comments
 (0)