Skip to content

Commit 262d9e9

Browse files
author
Lifeng Lu
committed
Prevent blocking thread pool during initialization.
1 parent 56ccd0a commit 262d9e9

File tree

2 files changed

+36
-34
lines changed

2 files changed

+36
-34
lines changed

src/Microsoft.VisualStudio.ProjectSystem.Managed/GlobalSuppressions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,3 @@
2424
[assembly: SuppressMessage("Style", "VSTHRD200:Use \"Async\" suffix for async methods", Justification = "By design", Scope = "member", Target = "~M:Microsoft.VisualStudio.Threading.TaskResult.EmptyEnumerable``1~System.Threading.Tasks.Task{System.Collections.Generic.IEnumerable{``0}}")]
2525
[assembly: SuppressMessage("Style", "VSTHRD200:Use \"Async\" suffix for async methods", Justification = "<Pending>", Scope = "member", Target = "~M:Microsoft.VisualStudio.ProjectSystem.Debug.LaunchSettingsProviderExtensions.WaitForFirstSnapshot(Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchSettingsProvider,System.Threading.CancellationToken)~System.Threading.Tasks.Task{Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchSettings}")]
2626

27-
[assembly: SuppressMessage("Usage", "VSTHRD102:Implement internal logic asynchronously", Justification = "<Pending>", Scope = "member", Target = "~M:Microsoft.VisualStudio.ProjectSystem.Debug.LaunchSettingsProvider.WatchLaunchSettingsFile")]

src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/LaunchSettingsProvider.cs

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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,13 @@ 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+
// establish the file watcher. We don't need wait this, because files can be changed in the system anyway, so blocking our process
224+
// doesn't provide real benefit. It is of course possible that the file is changed before the watcher is established. To eliminate this
225+
// gap, we can recheck file after the watcher is established. I will skip this for now.
226+
_project.Services.FaultHandler.Forget(
227+
_launchSettingFileWatcher.GetValueAsync(),
228+
_project,
229+
severity: ProjectFaultSeverity.LimitedFunctionality);
222230
}
223231

224232
/// <summary>
@@ -547,53 +555,48 @@ protected async Task EnsureSettingsFolderAsync()
547555
/// Sets up a file system watcher to look for changes to the launchsettings.json file. It watches at the root of the
548556
/// project otherwise we force the project to have a properties folder.
549557
/// </summary>
550-
private void WatchLaunchSettingsFile()
558+
private async Task<IFileWatcher?> WatchLaunchSettingsFileAsync()
551559
{
552-
if (_launchSettingFileWatcher is null)
553-
{
554-
FileChangeScheduler?.Dispose();
560+
FileChangeScheduler?.Dispose();
561+
562+
Assumes.Present(_projectServices.ProjectAsynchronousTasks);
555563

556-
Assumes.Present(_projectServices.ProjectAsynchronousTasks);
564+
CancellationToken cancellationToken = _projectServices.ProjectAsynchronousTasks.UnloadCancellationToken;
557565

558-
// Create our scheduler for processing file changes
559-
FileChangeScheduler = new TaskDelayScheduler(FileChangeProcessingDelay, _commonProjectServices.ThreadingService,
560-
_projectServices.ProjectAsynchronousTasks.UnloadCancellationToken);
566+
// Create our scheduler for processing file changes
567+
FileChangeScheduler = new TaskDelayScheduler(FileChangeProcessingDelay, _commonProjectServices.ThreadingService, cancellationToken);
568+
569+
IFileWatcher? fileWatcher = null;
570+
571+
string launchSettingsToWatch = await GetLaunchSettingsFilePathAsync();
572+
if (launchSettingsToWatch is not null)
573+
{
574+
fileWatcher = await _fileWatcherService.CreateFileWatcherAsync(this, FileWatchChangeKinds.Changed | FileWatchChangeKinds.Added | FileWatchChangeKinds.Removed, cancellationToken);
561575

562576
try
563577
{
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-
});
578+
// disposing the file watcher will unregister the file, so we don't have to track the cookie.
579+
_ = await fileWatcher.RegisterFileAsync(launchSettingsToWatch, cancellationToken);
573580
}
574581
catch (Exception ex) when (ex is IOException || ex is ArgumentException)
575582
{
576583
// If the project folder is no longer available this will throw, which can happen during branch switching
584+
await fileWatcher.DisposeAsync();
585+
fileWatcher = null;
577586
}
578587
}
588+
589+
return fileWatcher;
579590
}
580591

581592
protected override void Dispose(bool disposing)
582593
{
583594
if (disposing)
584595
{
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-
}
596+
_project.Services.FaultHandler.Forget(
597+
_launchSettingFileWatcher.DisposeValueAsync(),
598+
_project,
599+
severity: ProjectFaultSeverity.Recoverable);
597600

598601
if (FileChangeScheduler is not null)
599602
{

0 commit comments

Comments
 (0)