@@ -61,9 +61,8 @@ internal class LaunchSettingsProvider : ProjectValueDataSourceBase<ILaunchSettin
6161 private readonly AsyncLazy < string > _launchSettingsFilePath ;
6262 private readonly SequentialTaskExecutor _sequentialTaskQueue ;
6363 private readonly Lazy < LaunchProfile ? > _defaultLaunchProfile ;
64+ private readonly AsyncLazy < IFileWatcher ? > _launchSettingFileWatcher ;
6465
65- private IFileWatcher ? _launchSettingFileWatcher ;
66- private int _launchSettingFileWatcherCookie ;
6766 private IReceivableSourceBlock < ILaunchSettings > ? _changedSourceBlock ;
6867 private IBroadcastBlock < ILaunchSettings > ? _broadcastBlock ;
6968 private IReceivableSourceBlock < IProjectVersionedValue < ILaunchSettings > > ? _versionedChangedSourceBlock ;
@@ -103,6 +102,10 @@ public LaunchSettingsProvider(
103102 _sequentialTaskQueue = new SequentialTaskExecutor ( new JoinableTaskContextNode ( joinableTaskContext ) , nameof ( LaunchSettingsProvider ) ) ;
104103
105104 _launchSettingsFilePath = new AsyncLazy < string > ( GetLaunchSettingsFilePathNoCacheAsync , commonProjectServices . ThreadingService . JoinableTaskFactory ) ;
105+ _launchSettingFileWatcher = new AsyncLazy < IFileWatcher ? > ( WatchLaunchSettingsFileAsync , commonProjectServices . ThreadingService . JoinableTaskFactory )
106+ {
107+ SuppressRecursiveFactoryDetection = true
108+ } ;
106109
107110 _defaultLaunchProfile = new Lazy < LaunchProfile ? > ( ( ) =>
108111 {
@@ -217,8 +220,24 @@ protected override void Initialize()
217220 JoinUpstreamDataSources ( _projectSubscriptionService . ProjectRuleSource , _commonProjectServices . Project . Capabilities ) ;
218221 }
219222
220- // Make sure we are watching the file at this point
221- WatchLaunchSettingsFile ( ) ;
223+ FileChangeScheduler ? . Dispose ( ) ;
224+
225+ Assumes . Present ( _projectServices . ProjectAsynchronousTasks ) ;
226+
227+ // Create our scheduler for processing file changes
228+ FileChangeScheduler = new TaskDelayScheduler (
229+ FileChangeProcessingDelay ,
230+ _commonProjectServices . ThreadingService ,
231+ _projectServices . ProjectAsynchronousTasks . UnloadCancellationToken ) ;
232+
233+
234+ // establish the file watcher. We don't need wait this, because files can be changed in the system anyway, so blocking our process
235+ // doesn't provide real benefit. It is of course possible that the file is changed before the watcher is established. To eliminate this
236+ // gap, we can recheck file after the watcher is established. I will skip this for now.
237+ _project . Services . FaultHandler . Forget (
238+ _launchSettingFileWatcher . GetValueAsync ( ) ,
239+ _project ,
240+ severity : ProjectFaultSeverity . LimitedFunctionality ) ;
222241 }
223242
224243 /// <summary>
@@ -517,16 +536,23 @@ protected async Task HandleLaunchSettingsFileChangedAsync()
517536 {
518537 Assumes . NotNull ( FileChangeScheduler ) ;
519538
520- await FileChangeScheduler . ScheduleAsyncTask ( token =>
539+ try
521540 {
522- if ( token . IsCancellationRequested )
541+ await FileChangeScheduler . ScheduleAsyncTask ( token =>
523542 {
524- return Task . CompletedTask ;
525- }
543+ if ( token . IsCancellationRequested )
544+ {
545+ return Task . CompletedTask ;
546+ }
526547
527- // Updates need to be sequenced
528- return _sequentialTaskQueue . ExecuteTask ( ( ) => UpdateProfilesAsync ( null ) ) ;
529- } ) ;
548+ // Updates need to be sequenced
549+ return _sequentialTaskQueue . ExecuteTask ( ( ) => UpdateProfilesAsync ( null ) ) ;
550+ } ) ;
551+ }
552+ catch ( ObjectDisposedException )
553+ {
554+ // during closing the FileChangeScheduler can be disposed while the task to process the last file change is still running.
555+ }
530556 }
531557 }
532558
@@ -547,53 +573,42 @@ protected async Task EnsureSettingsFolderAsync()
547573 /// Sets up a file system watcher to look for changes to the launchsettings.json file. It watches at the root of the
548574 /// project otherwise we force the project to have a properties folder.
549575 /// </summary>
550- private void WatchLaunchSettingsFile ( )
576+ private async Task < IFileWatcher ? > WatchLaunchSettingsFileAsync ( )
551577 {
552- if ( _launchSettingFileWatcher is null )
553- {
554- FileChangeScheduler ? . Dispose ( ) ;
578+ Assumes . Present ( _projectServices . ProjectAsynchronousTasks ) ;
579+ CancellationToken cancellationToken = _projectServices . ProjectAsynchronousTasks . UnloadCancellationToken ;
555580
556- Assumes . Present ( _projectServices . ProjectAsynchronousTasks ) ;
581+ IFileWatcher ? fileWatcher = null ;
557582
558- // Create our scheduler for processing file changes
559- FileChangeScheduler = new TaskDelayScheduler ( FileChangeProcessingDelay , _commonProjectServices . ThreadingService ,
560- _projectServices . ProjectAsynchronousTasks . UnloadCancellationToken ) ;
583+ string launchSettingsToWatch = await GetLaunchSettingsFilePathAsync ( ) ;
584+ if ( launchSettingsToWatch is not null )
585+ {
586+ fileWatcher = await _fileWatcherService . CreateFileWatcherAsync ( this , FileWatchChangeKinds . Changed | FileWatchChangeKinds . Added | FileWatchChangeKinds . Removed , cancellationToken ) ;
561587
562588 try
563589 {
564- JoinableFactory . Run ( async ( ) =>
565- {
566- string launchSettingsToWatch = await GetLaunchSettingsFilePathAsync ( ) ;
567- if ( launchSettingsToWatch is not null )
568- {
569- _launchSettingFileWatcher = await _fileWatcherService . CreateFileWatcherAsync ( this , FileWatchChangeKinds . Changed | FileWatchChangeKinds . Added | FileWatchChangeKinds . Removed , CancellationToken . None ) ;
570- _launchSettingFileWatcherCookie = await _launchSettingFileWatcher . RegisterFileAsync ( launchSettingsToWatch , CancellationToken . None ) ;
571- }
572- } ) ;
590+ // disposing the file watcher will unregister the file, so we don't have to track the cookie.
591+ _ = await fileWatcher . RegisterFileAsync ( launchSettingsToWatch , cancellationToken ) ;
573592 }
574593 catch ( Exception ex ) when ( ex is IOException || ex is ArgumentException )
575594 {
576595 // If the project folder is no longer available this will throw, which can happen during branch switching
596+ await fileWatcher . DisposeAsync ( ) ;
597+ fileWatcher = null ;
577598 }
578599 }
600+
601+ return fileWatcher ;
579602 }
580603
581604 protected override void Dispose ( bool disposing )
582605 {
583606 if ( disposing )
584607 {
585- if ( _launchSettingFileWatcher is not null )
586- {
587- if ( _launchSettingFileWatcherCookie is not 0 )
588- {
589- // Unregister the file watcher
590- _launchSettingFileWatcher . Unregister ( _launchSettingFileWatcherCookie ) ;
591- _launchSettingFileWatcherCookie = 0 ;
592- }
593-
594- _launchSettingFileWatcher . Dispose ( ) ;
595- _launchSettingFileWatcher = null ;
596- }
608+ _project . Services . FaultHandler . Forget (
609+ _launchSettingFileWatcher . DisposeValueAsync ( ) ,
610+ _project ,
611+ severity : ProjectFaultSeverity . Recoverable ) ;
597612
598613 if ( FileChangeScheduler is not null )
599614 {
0 commit comments