Skip to content

Commit

Permalink
Merge pull request dotnet#3094 from Pilchie/Fix2708-ProjectAssetFileW…
Browse files Browse the repository at this point in the history
…atcher

Connect to the tree provider asynchronously.
  • Loading branch information
Pilchie authored Jan 10, 2018
2 parents 613e0d0 + c8f2dbb commit e1bc62e
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;

using Task = System.Threading.Tasks.Task;

namespace Microsoft.VisualStudio.ProjectSystem
{
// Mocks a System.IService provider to return Moqs of IVS* services.
public class IAsyncServiceProviderMoq : IAsyncServiceProvider
{
// Usage. Create a new IAsyncServiceProviderMoq and add your service moqs to it.
private Dictionary<Type, object> Services = new Dictionary<Type, object>();

// Returns null if it can't get it
public Task<object> GetServiceAsync(Type serviceType)
{
Services.TryGetValue(serviceType, out object retVal);

return Task.FromResult(retVal);
}

public void AddService(Type interfaceType, Type serviceType, object serviceMock)
{
Services.Add(serviceType, serviceMock);
if (serviceType != interfaceType)
{
Services.Add(interfaceType, serviceMock);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell.Interop;
using Moq;
using Xunit;
Expand Down Expand Up @@ -30,23 +31,24 @@ public class ProjectAssetFileWatcherTests
[InlineData(@"
Root (flags: {ProjectRoot}), FilePath: ""C:\Foo\foo.proj""
foo.project.json, FilePath: ""C:\Foo\foo.project.json""", @"C:\Foo\foo.project.lock.json")]
public void VerifyFileWatcherRegistration(string inputTree, string fileToWatch)
public async Task VerifyFileWatcherRegistration(string inputTree, string fileToWatch)
{
var spMock = new IServiceProviderMoq();
var spMock = new IAsyncServiceProviderMoq();
uint adviseCookie = 100;
var fileChangeService = IVsFileChangeExFactory.CreateWithAdviseUnadviseFileChange(adviseCookie);
spMock.AddService(typeof(IVsFileChangeEx), typeof(SVsFileChangeEx), fileChangeService);
var tasksService = IUnconfiguredProjectTasksServiceFactory.ImplementLoadedProjectAsync<ConfiguredProject>(t => t());

var watcher = new ProjectAssetFileWatcher(spMock,
IProjectTreeProviderFactory.Create(),
IUnconfiguredProjectCommonServicesFactory.Create(),
IProjectLockServiceFactory.Create(),
IUnconfiguredProjectCommonServicesFactory.Create(threadingService: new IProjectThreadingServiceMock()),
tasksService,
IActiveConfiguredProjectSubscriptionServiceFactory.CreateInstance());

var tree = ProjectTreeParser.Parse(inputTree);
var projectUpdate = IProjectSubscriptionUpdateFactory.FromJson(ProjectCurrentStateJson);
watcher.Load();
watcher.DataFlow_Changed(IProjectVersionedValueFactory<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>.Create((Tuple.Create(IProjectTreeSnapshotFactory.Create(tree), projectUpdate))));
await watcher.DataFlow_ChangedAsync(IProjectVersionedValueFactory<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>.Create((Tuple.Create(IProjectTreeSnapshotFactory.Create(tree), projectUpdate))));

// If fileToWatch is null then we expect to not register any filewatcher.
var times = fileToWatch == null ? Times.Never() : Times.Once();
Expand Down Expand Up @@ -84,26 +86,27 @@ public void VerifyFileWatcherRegistration(string inputTree, string fileToWatch)
project.json, FilePath: ""C:\Foo\project.json""
somefile.json, FilePath: ""C:\Foo\somefile.json""", 1, 0)]

public void VerifyFileWatcherRegistrationOnTreeChange(string inputTree, string changedTree, int numRegisterCalls, int numUnregisterCalls)
public async Task VerifyFileWatcherRegistrationOnTreeChange(string inputTree, string changedTree, int numRegisterCalls, int numUnregisterCalls)
{
var spMock = new IServiceProviderMoq();
var spMock = new IAsyncServiceProviderMoq();
uint adviseCookie = 100;
var fileChangeService = IVsFileChangeExFactory.CreateWithAdviseUnadviseFileChange(adviseCookie);
spMock.AddService(typeof(IVsFileChangeEx), typeof(SVsFileChangeEx), fileChangeService);
var tasksService = IUnconfiguredProjectTasksServiceFactory.ImplementLoadedProjectAsync<ConfiguredProject>(t => t());

var watcher = new ProjectAssetFileWatcher(spMock,
IProjectTreeProviderFactory.Create(),
IUnconfiguredProjectCommonServicesFactory.Create(),
IProjectLockServiceFactory.Create(),
IUnconfiguredProjectCommonServicesFactory.Create(threadingService: new IProjectThreadingServiceMock()),
tasksService,
IActiveConfiguredProjectSubscriptionServiceFactory.CreateInstance());
watcher.Load();
var projectUpdate = IProjectSubscriptionUpdateFactory.FromJson(ProjectCurrentStateJson);

var firstTree = ProjectTreeParser.Parse(inputTree);
watcher.DataFlow_Changed(IProjectVersionedValueFactory<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>.Create((Tuple.Create(IProjectTreeSnapshotFactory.Create(firstTree), projectUpdate))));
await watcher.DataFlow_ChangedAsync(IProjectVersionedValueFactory<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>.Create((Tuple.Create(IProjectTreeSnapshotFactory.Create(firstTree), projectUpdate))));

var secondTree = ProjectTreeParser.Parse(changedTree);
watcher.DataFlow_Changed(IProjectVersionedValueFactory<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>.Create((Tuple.Create(IProjectTreeSnapshotFactory.Create(secondTree), projectUpdate))));
await watcher.DataFlow_ChangedAsync(IProjectVersionedValueFactory<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>.Create((Tuple.Create(IProjectTreeSnapshotFactory.Create(secondTree), projectUpdate))));

// If fileToWatch is null then we expect to not register any filewatcher.
var fileChangeServiceMock = Mock.Get(fileChangeService);
Expand All @@ -113,16 +116,17 @@ public void VerifyFileWatcherRegistrationOnTreeChange(string inputTree, string c
}

[Fact]
public void WhenBaseIntermediateOutputPathNotSet_DoesNotAttemptToAdviseFileChange()
public async Task WhenBaseIntermediateOutputPathNotSet_DoesNotAttemptToAdviseFileChange()
{
var spMock = new IServiceProviderMoq();
var spMock = new IAsyncServiceProviderMoq();
var fileChangeService = IVsFileChangeExFactory.CreateWithAdviseUnadviseFileChange(100);
spMock.AddService(typeof(IVsFileChangeEx), typeof(SVsFileChangeEx), fileChangeService);
var tasksService = IUnconfiguredProjectTasksServiceFactory.ImplementLoadedProjectAsync<ConfiguredProject>(t => t());

var watcher = new ProjectAssetFileWatcher(spMock,
IProjectTreeProviderFactory.Create(),
IUnconfiguredProjectCommonServicesFactory.Create(),
IProjectLockServiceFactory.Create(),
IUnconfiguredProjectCommonServicesFactory.Create(threadingService: new IProjectThreadingServiceMock()),
tasksService,
IActiveConfiguredProjectSubscriptionServiceFactory.CreateInstance());

var tree = ProjectTreeParser.Parse(@"Root (flags: {ProjectRoot}), FilePath: ""C:\Foo\foo.proj""");
Expand All @@ -137,7 +141,7 @@ public void WhenBaseIntermediateOutputPathNotSet_DoesNotAttemptToAdviseFileChang
}");

watcher.Load();
watcher.DataFlow_Changed(IProjectVersionedValueFactory<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>.Create((Tuple.Create(IProjectTreeSnapshotFactory.Create(tree), projectUpdate))));
await watcher.DataFlow_ChangedAsync(IProjectVersionedValueFactory<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>.Create((Tuple.Create(IProjectTreeSnapshotFactory.Create(tree), projectUpdate))));

var fileChangeServiceMock = Mock.Get(fileChangeService);
uint cookie;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,27 @@
using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;

using IAsyncServiceProvider = Microsoft.VisualStudio.Shell.IAsyncServiceProvider;
using Task = System.Threading.Tasks.Task;

namespace Microsoft.VisualStudio.ProjectSystem.VS.NuGet
{
/// <summary>
/// Watches for writes to the project.assets.json, triggering a evaluation if it changes.
/// </summary>
internal class ProjectAssetFileWatcher : OnceInitializedOnceDisposed, IVsFileChangeEvents
internal class ProjectAssetFileWatcher : OnceInitializedOnceDisposedAsync, IVsFileChangeEvents
{
private readonly IServiceProvider _serviceProvider;
private readonly IAsyncServiceProvider _asyncServiceProvider;
private readonly IUnconfiguredProjectCommonServices _projectServices;
private readonly IProjectLockService _projectLockService;
private readonly IUnconfiguredProjectTasksService _projectTasksService;
private readonly IActiveConfiguredProjectSubscriptionService _activeConfiguredProjectSubscriptionService;
private readonly IProjectTreeProvider _fileSystemTreeProvider;
private IVsFileChangeEx _fileChangeService;
Expand All @@ -26,63 +32,89 @@ internal class ProjectAssetFileWatcher : OnceInitializedOnceDisposed, IVsFileCha
private string _fileBeingWatched;

[ImportingConstructor]
public ProjectAssetFileWatcher([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider,
[Import(ContractNames.ProjectTreeProviders.FileSystemDirectoryTree)] IProjectTreeProvider fileSystemTreeProvider,
IUnconfiguredProjectCommonServices projectServices,
IProjectLockService projectLockService,
IActiveConfiguredProjectSubscriptionService activeConfiguredProjectSubscriptionService)
public ProjectAssetFileWatcher(
[Import(ContractNames.ProjectTreeProviders.FileSystemDirectoryTree)] IProjectTreeProvider fileSystemTreeProvider,
IUnconfiguredProjectCommonServices projectServices,
IUnconfiguredProjectTasksService projectTasksService,
IActiveConfiguredProjectSubscriptionService activeConfiguredProjectSubscriptionService)
: this(
AsyncServiceProvider.GlobalProvider,
fileSystemTreeProvider,
projectServices,
projectTasksService,
activeConfiguredProjectSubscriptionService)
{
}

public ProjectAssetFileWatcher(
IAsyncServiceProvider asyncServiceProvider,
IProjectTreeProvider fileSystemTreeProvider,
IUnconfiguredProjectCommonServices projectServices,
IUnconfiguredProjectTasksService projectTasksService,
IActiveConfiguredProjectSubscriptionService activeConfiguredProjectSubscriptionService)
: base(projectServices.ThreadingService.JoinableTaskContext)
{
Requires.NotNull(serviceProvider, nameof(serviceProvider));
Requires.NotNull(asyncServiceProvider, nameof(asyncServiceProvider));
Requires.NotNull(fileSystemTreeProvider, nameof(fileSystemTreeProvider));
Requires.NotNull(projectServices, nameof(projectServices));
Requires.NotNull(projectLockService, nameof(projectLockService));
Requires.NotNull(projectTasksService, nameof(projectTasksService));
Requires.NotNull(activeConfiguredProjectSubscriptionService, nameof(activeConfiguredProjectSubscriptionService));

_serviceProvider = serviceProvider;
_asyncServiceProvider = asyncServiceProvider;
_fileSystemTreeProvider = fileSystemTreeProvider;
_projectServices = projectServices;
_projectLockService = projectLockService;
_projectTasksService = projectTasksService;
_activeConfiguredProjectSubscriptionService = activeConfiguredProjectSubscriptionService;
}

/// <summary>
/// Called on project load.
/// </summary>
[ConfiguredProjectAutoLoad(RequiresUIThread = true)]
[ConfiguredProjectAutoLoad(RequiresUIThread = false)]
[AppliesTo(ProjectCapability.CSharpOrVisualBasicOrFSharp)]
internal void Load()
{
EnsureInitialized();
InitializeAsync();
}

/// <summary>
/// Initialize the watcher.
/// </summary>
protected override void Initialize()
protected override async Task InitializeCoreAsync(CancellationToken cancellationToken)
{
_fileChangeService = _serviceProvider.GetService<IVsFileChangeEx, SVsFileChangeEx>();
_fileChangeService = (IVsFileChangeEx)(await _asyncServiceProvider.GetServiceAsync(typeof(SVsFileChangeEx)).ConfigureAwait(false));

// The tree source to get changes to the tree so that we can identify when the assets file changes.
var treeSource = _fileSystemTreeProvider.Tree.SyncLinkOptions();
// Explicitly get back to the thread pool for the rest of this method so we don't tie up the UI thread;
await TaskScheduler.Default;

// The property source used to get the value of the $ProjectAssetsFile property so that we can identify the location of the assets file.
var sourceLinkOptions = new StandardRuleDataflowLinkOptions
{
RuleNames = Empty.OrdinalIgnoreCaseStringSet.Add(ConfigurationGeneral.SchemaName),
PropagateCompletion = true
};
var propertySource = _activeConfiguredProjectSubscriptionService.ProjectRuleSource.SourceBlock.SyncLinkOptions(sourceLinkOptions);
var target = new ActionBlock<IProjectVersionedValue<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>>(new Action<IProjectVersionedValue<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>>(DataFlow_Changed));

// Join the two sources so that we get synchronized versions of the data.
_treeWatcher = ProjectDataSources.SyncLinkTo(treeSource, propertySource, target);
await _projectTasksService.LoadedProjectAsync(() =>
{
// The tree source to get changes to the tree so that we can identify when the assets file changes.
var treeSource = _fileSystemTreeProvider.Tree.SyncLinkOptions();
// The property source used to get the value of the $ProjectAssetsFile property so that we can identify the location of the assets file.
var sourceLinkOptions = new StandardRuleDataflowLinkOptions
{
RuleNames = Empty.OrdinalIgnoreCaseStringSet.Add(ConfigurationGeneral.SchemaName),
PropagateCompletion = true
};
var propertySource = _activeConfiguredProjectSubscriptionService.ProjectRuleSource.SourceBlock.SyncLinkOptions(sourceLinkOptions);
var target = new ActionBlock<IProjectVersionedValue<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>>>(DataFlow_ChangedAsync);
// Join the two sources so that we get synchronized versions of the data.
_treeWatcher = ProjectDataSources.SyncLinkTo(treeSource, propertySource, target);
return Task.CompletedTask;
}).ConfigureAwait(false);
}

/// <summary>
/// Called on changes to the project tree.
/// </summary>
internal void DataFlow_Changed(IProjectVersionedValue<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>> dataFlowUpdate)
internal async Task DataFlow_ChangedAsync(IProjectVersionedValue<Tuple<IProjectTreeSnapshot, IProjectSubscriptionUpdate>> dataFlowUpdate)
{
await InitializeAsync().ConfigureAwait(false);

var treeSnapshot = dataFlowUpdate.Value.Item1;
var newTree = treeSnapshot.Tree;
if (newTree == null)
Expand Down Expand Up @@ -175,13 +207,15 @@ private void UnregisterFileWatcherIfAny()
}
}

protected override void Dispose(bool disposing)
protected override Task DisposeCoreAsync(bool initialized)
{
if (disposing)
if (initialized)
{
_treeWatcher.Dispose();
UnregisterFileWatcherIfAny();
}

return Task.CompletedTask;
}

/// <summary>
Expand All @@ -197,7 +231,7 @@ public int FilesChanged(uint cChanges, string[] rgpszFile, uint[] rggrfChange)
{
await _projectServices.Project.Services.ProjectAsynchronousTasks.LoadedProjectAsync(async () =>
{
using (var access = await _projectLockService.WriteLockAsync())
using (var access = await _projectServices.ProjectLockService.WriteLockAsync())
{
// notify all the loaded configured projects
var currentProjects = _projectServices.Project.LoadedConfiguredProjects;
Expand Down

0 comments on commit e1bc62e

Please sign in to comment.