@@ -15,7 +15,7 @@ internal sealed class CompilationHandler : IDisposable
15
15
{
16
16
public readonly IncrementalMSBuildWorkspace Workspace ;
17
17
public readonly EnvironmentOptions EnvironmentOptions ;
18
-
18
+ private readonly GlobalOptions _options ;
19
19
private readonly IReporter _reporter ;
20
20
private readonly WatchHotReloadService _hotReloadService ;
21
21
@@ -41,10 +41,11 @@ internal sealed class CompilationHandler : IDisposable
41
41
42
42
private bool _isDisposed ;
43
43
44
- public CompilationHandler ( IReporter reporter , EnvironmentOptions environmentOptions , CancellationToken shutdownCancellationToken )
44
+ public CompilationHandler ( IReporter reporter , EnvironmentOptions environmentOptions , GlobalOptions options , CancellationToken shutdownCancellationToken )
45
45
{
46
46
_reporter = reporter ;
47
47
EnvironmentOptions = environmentOptions ;
48
+ _options = options ;
48
49
Workspace = new IncrementalMSBuildWorkspace ( reporter ) ;
49
50
_hotReloadService = new WatchHotReloadService ( Workspace . CurrentSolution . Services , ( ) => ValueTask . FromResult ( GetAggregateCapabilities ( ) ) ) ;
50
51
_shutdownCancellationToken = shutdownCancellationToken ;
@@ -256,41 +257,25 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
256
257
var currentSolution = Workspace . CurrentSolution ;
257
258
var runningProjects = _runningProjects ;
258
259
259
- var runningProjectIds = currentSolution . Projects
260
- . Where ( project => project . FilePath != null && runningProjects . ContainsKey ( project . FilePath ) )
261
- . Select ( project => project . Id )
262
- . ToImmutableHashSet ( ) ;
260
+ var runningProjectInfos =
261
+ ( from project in currentSolution . Projects
262
+ let runningProject = GetCorrespondingRunningProject ( project , runningProjects )
263
+ where runningProject != null
264
+ let autoRestart = _options . NonInteractive || runningProject . ProjectNode . IsAutoRestartEnabled ( )
265
+ select ( project . Id , info : new WatchHotReloadService . RunningProjectInfo ( ) { RestartWhenChangesHaveNoEffect = autoRestart } ) )
266
+ . ToImmutableDictionary ( e => e . Id , e => e . info ) ;
263
267
264
- var updates = await _hotReloadService . GetUpdatesAsync ( currentSolution , runningProjectIds , cancellationToken ) ;
265
- var anyProcessNeedsRestart = ! updates . ProjectIdsToRestart . IsEmpty ;
268
+ var updates = await _hotReloadService . GetUpdatesAsync ( currentSolution , runningProjectInfos , cancellationToken ) ;
266
269
267
- await DisplayResultsAsync ( updates , cancellationToken ) ;
270
+ await DisplayResultsAsync ( updates , runningProjectInfos , cancellationToken ) ;
268
271
269
- if ( updates . Status is ModuleUpdateStatus . None or ModuleUpdateStatus . Blocked )
272
+ if ( updates . Status is WatchHotReloadService . Status . NoChangesToApply or WatchHotReloadService . Status . Blocked )
270
273
{
271
274
// If Hot Reload is blocked (due to compilation error) we ignore the current
272
275
// changes and await the next file change.
273
276
return ( ImmutableDictionary < ProjectId , string > . Empty , [ ] ) ;
274
277
}
275
278
276
- if ( updates . Status == ModuleUpdateStatus . RestartRequired )
277
- {
278
- if ( ! anyProcessNeedsRestart )
279
- {
280
- return ( ImmutableDictionary < ProjectId , string > . Empty , [ ] ) ;
281
- }
282
-
283
- await restartPrompt . Invoke ( updates . ProjectIdsToRestart . Select ( id => currentSolution . GetProject ( id ) ! . Name ) , cancellationToken ) ;
284
-
285
- // Terminate all tracked processes that need to be restarted,
286
- // except for the root process, which will terminate later on.
287
- var terminatedProjects = await TerminateNonRootProcessesAsync ( updates . ProjectIdsToRestart . Select ( id => currentSolution . GetProject ( id ) ! . FilePath ! ) , cancellationToken ) ;
288
-
289
- return ( updates . ProjectIdsToRebuild . ToImmutableDictionary ( keySelector : id => id , elementSelector : id => currentSolution . GetProject ( id ) ! . FilePath ! ) , terminatedProjects ) ;
290
- }
291
-
292
- Debug . Assert ( updates . Status == ModuleUpdateStatus . Ready ) ;
293
-
294
279
ImmutableDictionary < string , ImmutableArray < RunningProject > > projectsToUpdate ;
295
280
lock ( _runningProjectsAndUpdatesGuard )
296
281
{
@@ -326,115 +311,151 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT
326
311
}
327
312
} , cancellationToken ) ;
328
313
329
- return ( ImmutableDictionary < ProjectId , string > . Empty , [ ] ) ;
314
+
315
+ if ( updates . ProjectsToRestart . IsEmpty )
316
+ {
317
+ return ( ImmutableDictionary < ProjectId , string > . Empty , [ ] ) ;
318
+ }
319
+
320
+ // Terminate projects that need restarting.
321
+
322
+ var projectsToPromptForRestart =
323
+ ( from projectId in updates . ProjectsToRestart . Keys
324
+ where ! runningProjectInfos [ projectId ] . RestartWhenChangesHaveNoEffect // equivallent to auto-restart
325
+ select currentSolution . GetProject ( projectId ) ! . Name ) . ToList ( ) ;
326
+
327
+ if ( projectsToPromptForRestart is not [ ] )
328
+ {
329
+ await restartPrompt . Invoke ( projectsToPromptForRestart , cancellationToken ) ;
330
+ }
331
+
332
+ // Terminate all tracked processes that need to be restarted,
333
+ // except for the root process, which will terminate later on.
334
+ var terminatedProjects = await TerminateNonRootProcessesAsync ( updates . ProjectsToRestart . Select ( e => currentSolution . GetProject ( e . Key ) ! . FilePath ! ) , cancellationToken ) ;
335
+ var projectsToRebuild = updates . ProjectsToRebuild . ToImmutableDictionary ( keySelector : id => id , elementSelector : id => currentSolution . GetProject ( id ) ! . FilePath ! ) ;
336
+
337
+ return ( projectsToRebuild , terminatedProjects ) ;
330
338
}
331
339
332
- private async ValueTask DisplayResultsAsync ( WatchHotReloadService . Updates updates , CancellationToken cancellationToken )
340
+ private static RunningProject ? GetCorrespondingRunningProject ( Project project , ImmutableDictionary < string , ImmutableArray < RunningProject > > runningProjects )
333
341
{
334
- var anyProcessNeedsRestart = ! updates . ProjectIdsToRestart . IsEmpty ;
342
+ if ( project . FilePath == null || ! runningProjects . TryGetValue ( project . FilePath , out var projectsWithPath ) )
343
+ {
344
+ return null ;
345
+ }
335
346
336
- switch ( updates . Status )
347
+ // msbuild workspace doesn't set TFM if the project is not multi-targeted
348
+ var tfm = WatchHotReloadService . GetTargetFramework ( project ) ;
349
+ if ( tfm == null )
337
350
{
338
- case ModuleUpdateStatus . None :
339
- _reporter . Report ( MessageDescriptor . NoCSharpChangesToApply ) ;
340
- break ;
351
+ return projectsWithPath [ 0 ] ;
352
+ }
341
353
342
- case ModuleUpdateStatus . Ready :
343
- break ;
354
+ return projectsWithPath . SingleOrDefault ( p => string . Equals ( p . ProjectNode . GetTargetFramework ( ) , tfm , StringComparison . OrdinalIgnoreCase ) ) ;
355
+ }
344
356
345
- case ModuleUpdateStatus . RestartRequired :
346
- if ( anyProcessNeedsRestart )
347
- {
348
- _reporter . Output ( "Unable to apply hot reload, restart is needed to apply the changes." ) ;
349
- }
350
- else
351
- {
352
- _reporter . Verbose ( "Rude edits detected but do not affect any running process" ) ;
353
- }
357
+ private async ValueTask DisplayResultsAsync ( WatchHotReloadService . Updates2 updates , ImmutableDictionary < ProjectId , WatchHotReloadService . RunningProjectInfo > runningProjectInfos , CancellationToken cancellationToken )
358
+ {
359
+ switch ( updates . Status )
360
+ {
361
+ case WatchHotReloadService . Status . ReadyToApply :
362
+ break ;
354
363
364
+ case WatchHotReloadService . Status . NoChangesToApply :
365
+ _reporter . Report ( MessageDescriptor . NoCSharpChangesToApply ) ;
355
366
break ;
356
367
357
- case ModuleUpdateStatus . Blocked :
368
+ case WatchHotReloadService . Status . Blocked :
358
369
_reporter . Output ( "Unable to apply hot reload due to compilation errors." ) ;
359
370
break ;
360
371
361
372
default :
362
373
throw new InvalidOperationException ( ) ;
363
374
}
364
375
365
- // Diagnostics include syntactic errors, semantic warnings/errors and rude edit warnigns/errors for members being updated.
376
+ if ( ! updates . ProjectsToRestart . IsEmpty )
377
+ {
378
+ _reporter . Output ( "Restart is needed to apply the changes." ) ;
379
+ }
366
380
367
- var diagnosticsToDisplay = new List < string > ( ) ;
381
+ var diagnosticsToDisplayInApp = new List < string > ( ) ;
368
382
369
383
// Display errors first, then warnings:
370
- Display ( MessageSeverity . Error ) ;
371
- Display ( MessageSeverity . Warning ) ;
384
+ ReportCompilationDiagnostics ( DiagnosticSeverity . Error ) ;
385
+ ReportCompilationDiagnostics ( DiagnosticSeverity . Warning ) ;
386
+ ReportRudeEdits ( ) ;
387
+
388
+ // report or clear diagnostics in the browser UI
389
+ await ForEachProjectAsync (
390
+ _runningProjects ,
391
+ ( project , cancellationToken ) => project . BrowserRefreshServer ? . ReportCompilationErrorsInBrowserAsync ( [ .. diagnosticsToDisplayInApp ] , cancellationToken ) . AsTask ( ) ?? Task . CompletedTask ,
392
+ cancellationToken ) ;
372
393
373
- void Display ( MessageSeverity severity )
394
+ void ReportCompilationDiagnostics ( DiagnosticSeverity severity )
374
395
{
375
- foreach ( var diagnostic in updates . Diagnostics )
396
+ foreach ( var diagnostic in updates . CompilationDiagnostics )
376
397
{
377
- MessageDescriptor descriptor ;
378
-
379
- if ( diagnostic . Id == "ENC0118" )
380
- {
381
- // Changing '<entry-point>' might not have any effect until the application is restarted.
382
- descriptor = MessageDescriptor . ApplyUpdate_ChangingEntryPoint ;
383
- }
384
- else if ( diagnostic . Id == "ENC1005" )
385
- {
386
- // TODO: This warning is overreported in cases when the solution contains projects that are not rebuilt (up-to-date)
387
- // and a document is updated that is linked to such a project and another "active" project.
388
- // E.g. multi-tfm projects where only one TFM is currently built/running.
389
-
390
- // Warning: The current content of source file 'D:\Temp\App\Program.cs' does not match the built source.
391
- // Any changes made to this file while debugging won't be applied until its content matches the built source.
392
- descriptor = MessageDescriptor . ApplyUpdate_FileContentDoesNotMatchBuiltSource ;
393
- }
394
- else if ( diagnostic . Id == "CS8002" )
398
+ if ( diagnostic . Id == "CS8002" )
395
399
{
396
400
// TODO: This is not a useful warning. Compiler shouldn't be reporting this on .NET/
397
401
// Referenced assembly '...' does not have a strong name"
398
402
continue ;
399
403
}
400
- else
401
- {
402
- // Use the default severity of the diagnostic as it conveys impact on Hot Reload
403
- // (ignore warnings as errors and other severity configuration).
404
- descriptor = diagnostic . DefaultSeverity switch
405
- {
406
- DiagnosticSeverity . Error => MessageDescriptor . ApplyUpdate_Error ,
407
- DiagnosticSeverity . Warning => MessageDescriptor . ApplyUpdate_Warning ,
408
- _ => MessageDescriptor . ApplyUpdate_Verbose ,
409
- } ;
410
- }
411
404
412
- if ( descriptor . Severity != severity )
405
+ if ( diagnostic . DefaultSeverity != severity )
413
406
{
414
407
continue ;
415
408
}
416
409
417
- // Do not report rude edits as errors/warnings if no running process is affected.
418
- if ( ! anyProcessNeedsRestart && diagnostic . Id is [ 'E' , 'N' , 'C' , >= '0' and <= '9' , ..] )
410
+ ReportDiagnostic ( diagnostic , GetMessageDescritor ( diagnostic ) ) ;
411
+ }
412
+ }
413
+
414
+ void ReportRudeEdits ( )
415
+ {
416
+ // Rude edits in projects that caused restart of a project that can be restarted automatically
417
+ // will be reported only as verbose output.
418
+ var projectsWithVerboseRudeEdits = updates . ProjectsToRestart
419
+ . Where ( e => runningProjectInfos . TryGetValue ( e . Key , out var info ) && info . RestartWhenChangesHaveNoEffect )
420
+ . SelectMany ( e => e . Value )
421
+ . ToImmutableHashSet ( ) ;
422
+
423
+ foreach ( var ( projectId , diagnostics ) in updates . RudeEdits )
424
+ {
425
+ foreach ( var diagnostic in diagnostics )
419
426
{
420
- descriptor = descriptor with { Severity = MessageSeverity . Verbose } ;
421
- }
427
+ var descriptor = GetMessageDescritor ( diagnostic ) ;
422
428
423
- var display = CSharpDiagnosticFormatter . Instance . Format ( diagnostic ) ;
424
- _reporter . Report ( descriptor , display ) ;
429
+ if ( projectsWithVerboseRudeEdits . Contains ( projectId ) )
430
+ {
431
+ descriptor = descriptor with { Severity = MessageSeverity . Verbose } ;
432
+ }
425
433
426
- if ( descriptor . TryGetMessage ( prefix : null , [ display ] , out var message ) )
427
- {
428
- diagnosticsToDisplay . Add ( message ) ;
434
+ ReportDiagnostic ( diagnostic , descriptor ) ;
429
435
}
430
436
}
431
437
}
432
438
433
- // report or clear diagnostics in the browser UI
434
- await ForEachProjectAsync (
435
- _runningProjects ,
436
- ( project , cancellationToken ) => project . BrowserRefreshServer ? . ReportCompilationErrorsInBrowserAsync ( diagnosticsToDisplay . ToImmutableArray ( ) , cancellationToken ) . AsTask ( ) ?? Task . CompletedTask ,
437
- cancellationToken ) ;
439
+ void ReportDiagnostic ( Diagnostic diagnostic , MessageDescriptor descriptor )
440
+ {
441
+ var display = CSharpDiagnosticFormatter . Instance . Format ( diagnostic ) ;
442
+ _reporter . Report ( descriptor , display ) ;
443
+
444
+ if ( descriptor . TryGetMessage ( prefix : null , [ display ] , out var message ) )
445
+ {
446
+ diagnosticsToDisplayInApp . Add ( message ) ;
447
+ }
448
+ }
449
+
450
+ // Use the default severity of the diagnostic as it conveys impact on Hot Reload
451
+ // (ignore warnings as errors and other severity configuration).
452
+ static MessageDescriptor GetMessageDescritor ( Diagnostic diagnostic )
453
+ => diagnostic . DefaultSeverity switch
454
+ {
455
+ DiagnosticSeverity . Error => MessageDescriptor . ApplyUpdate_Error ,
456
+ DiagnosticSeverity . Warning => MessageDescriptor . ApplyUpdate_Warning ,
457
+ _ => MessageDescriptor . ApplyUpdate_Verbose ,
458
+ } ;
438
459
}
439
460
440
461
public async ValueTask < bool > HandleStaticAssetChangesAsync ( IReadOnlyList < ChangedFile > files , ProjectNodeMap projectMap , CancellationToken cancellationToken )
0 commit comments