From 114148eda179141a5be8a3a136c4a2f27c874417 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 2 Nov 2017 16:13:11 -0700 Subject: [PATCH] Introduce ProjectManager class to manage MSBuild project files This is a pretty substantial change that reworks the core logic of the MSBuild project system to enable an important scenario: updating a project when several files change in quick succession. In order to fix an issue with OmniSharp not reloading and updating a project in response to a 'dotnet restore', we must watch four files that might be touched during a restore: * project.asset.json * .nuget.cache * .nuget.g.props * .nuget.g.targets To ensure that we don't reload and update a project multiple times in response to multiple file changes, this PR introduces a simple queue and processing loop using a TPL DataFlow BufferBlock. --- .../FileChangeType.cs | 2 +- .../v1/FilesChanged/FilesChangedRequest.cs | 1 + src/OmniSharp.MSBuild/MSBuildProjectSystem.cs | 637 ------------------ ...esolver.cs => PackageDependencyChecker.cs} | 46 +- .../ProjectFileInfo.ProjectData.cs | 79 ++- .../ProjectFile/ProjectFileInfo.cs | 23 +- .../ProjectFile/ProjectFileInfoCollection.cs | 28 +- .../ProjectFile/ProjectFileInfoExtensions.cs | 71 ++ .../ProjectFile/PropertyConverter.cs | 2 +- src/OmniSharp.MSBuild/ProjectLoader.cs | 35 +- src/OmniSharp.MSBuild/ProjectManager.cs | 469 +++++++++++++ src/OmniSharp.MSBuild/ProjectSystem.cs | 202 ++++++ .../MSBuildProjectSystemTests.cs | 8 +- .../ProjectFileInfoTests.cs | 2 +- tests/TestUtility/OmniSharpTestHost.cs | 11 +- 15 files changed, 917 insertions(+), 699 deletions(-) rename src/OmniSharp.Abstractions/{Models/v1/FilesChanged => FileWatching}/FileChangeType.cs (73%) delete mode 100644 src/OmniSharp.MSBuild/MSBuildProjectSystem.cs rename src/OmniSharp.MSBuild/{Resolution/PackageDependencyResolver.cs => PackageDependencyChecker.cs} (69%) create mode 100644 src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs create mode 100644 src/OmniSharp.MSBuild/ProjectManager.cs create mode 100644 src/OmniSharp.MSBuild/ProjectSystem.cs diff --git a/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FileChangeType.cs b/src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs similarity index 73% rename from src/OmniSharp.Abstractions/Models/v1/FilesChanged/FileChangeType.cs rename to src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs index 7297a8343a..218513e78b 100644 --- a/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FileChangeType.cs +++ b/src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs @@ -1,4 +1,4 @@ -namespace OmniSharp.Models.FilesChanged +namespace OmniSharp.FileWatching { public enum FileChangeType { diff --git a/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FilesChangedRequest.cs b/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FilesChangedRequest.cs index e61a2056f6..6b720de559 100644 --- a/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FilesChangedRequest.cs +++ b/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FilesChangedRequest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using OmniSharp.FileWatching; using OmniSharp.Mef; namespace OmniSharp.Models.FilesChanged diff --git a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs deleted file mode 100644 index 7d559e16ed..0000000000 --- a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs +++ /dev/null @@ -1,637 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Composition; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using OmniSharp.Eventing; -using OmniSharp.FileWatching; -using OmniSharp.Models.Events; -using OmniSharp.Models.FilesChanged; -using OmniSharp.Models.UpdateBuffer; -using OmniSharp.Models.WorkspaceInformation; -using OmniSharp.MSBuild.Discovery; -using OmniSharp.MSBuild.Logging; -using OmniSharp.MSBuild.Models; -using OmniSharp.MSBuild.Models.Events; -using OmniSharp.MSBuild.ProjectFile; -using OmniSharp.MSBuild.Resolution; -using OmniSharp.MSBuild.SolutionParsing; -using OmniSharp.Options; -using OmniSharp.Services; - -namespace OmniSharp.MSBuild -{ - [Export(typeof(IProjectSystem)), Shared] - public class MSBuildProjectSystem : IProjectSystem - { - private readonly IOmniSharpEnvironment _environment; - private readonly OmniSharpWorkspace _workspace; - private readonly ImmutableDictionary _propertyOverrides; - private readonly DotNetCliService _dotNetCli; - private readonly MetadataFileReferenceCache _metadataFileReferenceCache; - private readonly IEventEmitter _eventEmitter; - private readonly IFileSystemWatcher _fileSystemWatcher; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - private readonly PackageDependencyResolver _packageDepedencyResolver; - - private readonly object _gate = new object(); - private readonly Queue _projectsToProcess; - private readonly ProjectFileInfoCollection _projects; - - private ProjectLoader _loader; - private MSBuildOptions _options; - private string _solutionFileOrRootPath; - - public string Key { get; } = "MsBuild"; - public string Language { get; } = LanguageNames.CSharp; - public IEnumerable Extensions { get; } = new[] { ".cs" }; - - [ImportingConstructor] - public MSBuildProjectSystem( - IOmniSharpEnvironment environment, - OmniSharpWorkspace workspace, - IMSBuildLocator msbuildLocator, - DotNetCliService dotNetCliService, - MetadataFileReferenceCache metadataFileReferenceCache, - IEventEmitter eventEmitter, - IFileSystemWatcher fileSystemWatcher, - ILoggerFactory loggerFactory) - { - _environment = environment; - _workspace = workspace; - _propertyOverrides = msbuildLocator.RegisteredInstance.PropertyOverrides; - _dotNetCli = dotNetCliService; - _metadataFileReferenceCache = metadataFileReferenceCache; - _eventEmitter = eventEmitter; - _fileSystemWatcher = fileSystemWatcher; - _loggerFactory = loggerFactory; - - _projects = new ProjectFileInfoCollection(); - _projectsToProcess = new Queue(); - _logger = loggerFactory.CreateLogger(); - _packageDepedencyResolver = new PackageDependencyResolver(loggerFactory); - } - - public void Initalize(IConfiguration configuration) - { - _options = new MSBuildOptions(); - ConfigurationBinder.Bind(configuration, _options); - - if (_environment.LogLevel < LogLevel.Information) - { - var buildEnvironmentInfo = MSBuildHelpers.GetBuildEnvironmentInfo(); - _logger.LogDebug($"MSBuild environment: {Environment.NewLine}{buildEnvironmentInfo}"); - } - - _loader = new ProjectLoader(_options, _environment.TargetDirectory, _propertyOverrides, _loggerFactory); - - var initialProjectPaths = GetInitialProjectPaths(); - - foreach (var projectPath in initialProjectPaths) - { - if (!File.Exists(projectPath)) - { - _logger.LogWarning($"Found project that doesn't exist on disk: {projectPath}"); - continue; - } - - var project = LoadProject(projectPath); - if (project == null) - { - // Diagnostics reported while loading the project have already been logged. - continue; - } - - _projectsToProcess.Enqueue(project); - } - - ProcessProjects(); - } - - private IEnumerable GetInitialProjectPaths() - { - // If a solution was provided, use it. - if (!string.IsNullOrEmpty(_environment.SolutionFilePath)) - { - _solutionFileOrRootPath = _environment.SolutionFilePath; - return GetProjectPathsFromSolution(_environment.SolutionFilePath); - } - - // Otherwise, assume that the path provided is a directory and look for a solution there. - var solutionFilePath = FindSolutionFilePath(_environment.TargetDirectory, _logger); - if (!string.IsNullOrEmpty(solutionFilePath)) - { - _solutionFileOrRootPath = solutionFilePath; - return GetProjectPathsFromSolution(solutionFilePath); - } - - // Finally, if there isn't a single solution immediately available, - // Just process all of the projects beneath the root path. - _solutionFileOrRootPath = _environment.TargetDirectory; - return Directory.GetFiles(_environment.TargetDirectory, "*.csproj", SearchOption.AllDirectories); - } - - private IEnumerable GetProjectPathsFromSolution(string solutionFilePath) - { - _logger.LogInformation($"Detecting projects in '{solutionFilePath}'."); - - var solutionFile = SolutionFile.ParseFile(solutionFilePath); - var processedProjects = new HashSet(StringComparer.OrdinalIgnoreCase); - var result = new List(); - - foreach (var project in solutionFile.Projects) - { - if (project.IsSolutionFolder) - { - continue; - } - - // Solution files are assumed to contain relative paths to project files with Windows-style slashes. - var projectFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar); - projectFilePath = Path.Combine(_environment.TargetDirectory, projectFilePath); - projectFilePath = Path.GetFullPath(projectFilePath); - - // Have we seen this project? If so, move on. - if (processedProjects.Contains(projectFilePath)) - { - continue; - } - - if (string.Equals(Path.GetExtension(projectFilePath), ".csproj", StringComparison.OrdinalIgnoreCase)) - { - result.Add(projectFilePath); - } - - processedProjects.Add(projectFilePath); - } - - return result; - } - - private void ProcessProjects() - { - while (_projectsToProcess.Count > 0) - { - var newProjects = new List(); - - while (_projectsToProcess.Count > 0) - { - var project = _projectsToProcess.Dequeue(); - - if (!_projects.ContainsKey(project.FilePath)) - { - AddProject(project); - } - else - { - _projects[project.FilePath] = project; - } - - newProjects.Add(project); - } - - // Next, update all projects. - foreach (var project in newProjects) - { - UpdateProject(project); - } - - // Finally, check for any unresolved dependencies in the projects we just processes. - foreach (var project in newProjects) - { - CheckForUnresolvedDependences(project, allowAutoRestore: true); - } - } - } - - private void AddProject(ProjectFileInfo project) - { - _projects.Add(project); - - var compilationOptions = CreateCompilationOptions(project); - - var projectInfo = ProjectInfo.Create( - id: project.Id, - version: VersionStamp.Create(), - name: project.Name, - assemblyName: project.AssemblyName, - language: LanguageNames.CSharp, - filePath: project.FilePath, - outputFilePath: project.TargetPath, - compilationOptions: compilationOptions); - - _workspace.AddProject(projectInfo); - - WatchProject(project); - } - - private void WatchProject(ProjectFileInfo project) - { - // TODO: This needs some improvement. Currently, it tracks both deletions and changes - // as "updates". We should properly remove projects that are deleted. - _fileSystemWatcher.Watch(project.FilePath, (file, changeType) => - { - OnProjectChanged(project.FilePath, allowAutoRestore: true); - }); - - if (!string.IsNullOrEmpty(project.ProjectAssetsFile)) - { - _fileSystemWatcher.Watch(project.ProjectAssetsFile, (file, changeType) => - { - OnProjectChanged(project.FilePath, allowAutoRestore: false); - }); - } - } - - private static CSharpCompilationOptions CreateCompilationOptions(ProjectFileInfo projectFileInfo) - { - var result = new CSharpCompilationOptions(projectFileInfo.OutputKind); - - result = result.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default); - - if (projectFileInfo.AllowUnsafeCode) - { - result = result.WithAllowUnsafe(true); - } - - var specificDiagnosticOptions = new Dictionary(projectFileInfo.SuppressedDiagnosticIds.Count) - { - // Ensure that specific warnings about assembly references are always suppressed. - { "CS1701", ReportDiagnostic.Suppress }, - { "CS1702", ReportDiagnostic.Suppress }, - { "CS1705", ReportDiagnostic.Suppress } - }; - - if (projectFileInfo.SuppressedDiagnosticIds.Any()) - { - foreach (var id in projectFileInfo.SuppressedDiagnosticIds) - { - if (!specificDiagnosticOptions.ContainsKey(id)) - { - specificDiagnosticOptions.Add(id, ReportDiagnostic.Suppress); - } - } - } - - result = result.WithSpecificDiagnosticOptions(specificDiagnosticOptions); - - if (projectFileInfo.SignAssembly && !string.IsNullOrEmpty(projectFileInfo.AssemblyOriginatorKeyFile)) - { - var keyFile = Path.Combine(projectFileInfo.Directory, projectFileInfo.AssemblyOriginatorKeyFile); - result = result.WithStrongNameProvider(new DesktopStrongNameProvider()) - .WithCryptoKeyFile(keyFile); - } - - if (!string.IsNullOrWhiteSpace(projectFileInfo.DocumentationFile)) - { - result = result.WithXmlReferenceResolver(XmlFileResolver.Default); - } - - return result; - } - - private static string FindSolutionFilePath(string rootPath, ILogger logger) - { - var solutionsFilePaths = Directory.GetFiles(rootPath, "*.sln"); - var result = SolutionSelector.Pick(solutionsFilePaths, rootPath); - - if (result.Message != null) - { - logger.LogInformation(result.Message); - } - - return result.FilePath; - } - - private ProjectFileInfo LoadProject(string projectFilePath) - { - _logger.LogInformation($"Loading project: {projectFilePath}"); - - ProjectFileInfo project; - ImmutableArray diagnostics; - - try - { - (project, diagnostics) = ProjectFileInfo.Create(projectFilePath, _loader); - - if (project == null) - { - _logger.LogWarning($"Failed to load project file '{projectFilePath}'."); - } - } - catch (Exception ex) - { - _logger.LogWarning($"Failed to load project file '{projectFilePath}'.", ex); - _eventEmitter.Error(ex, fileName: projectFilePath); - project = null; - } - - _eventEmitter.MSBuildProjectDiagnostics(projectFilePath, diagnostics); - - return project; - } - - private void OnProjectChanged(string projectFilePath, bool allowAutoRestore) - { - lock (_gate) - { - if (_projects.TryGetValue(projectFilePath, out var oldProjectFileInfo)) - { - ProjectFileInfo newProjectFileInfo; - ImmutableArray diagnostics; - - (newProjectFileInfo, diagnostics) = oldProjectFileInfo.Reload(_loader); - - if (newProjectFileInfo != null) - { - _projects[projectFilePath] = newProjectFileInfo; - - UpdateProject(newProjectFileInfo); - CheckForUnresolvedDependences(newProjectFileInfo, allowAutoRestore); - } - } - - ProcessProjects(); - } - } - - private void UpdateProject(ProjectFileInfo projectFileInfo) - { - var project = _workspace.CurrentSolution.GetProject(projectFileInfo.Id); - if (project == null) - { - _logger.LogError($"Could not locate project in workspace: {projectFileInfo.FilePath}"); - return; - } - - UpdateSourceFiles(project, projectFileInfo.SourceFiles); - UpdateParseOptions(project, projectFileInfo.LanguageVersion, projectFileInfo.PreprocessorSymbolNames, !string.IsNullOrWhiteSpace(projectFileInfo.DocumentationFile)); - UpdateProjectReferences(project, projectFileInfo.ProjectReferences); - UpdateReferences(project, projectFileInfo.References); - } - - private void UpdateSourceFiles(Project project, IList sourceFiles) - { - var currentDocuments = project.Documents.ToDictionary(d => d.FilePath, d => d.Id); - - // Add source files to the project. - foreach (var sourceFile in sourceFiles) - { - WatchDirectoryContainingFile(sourceFile); - - // If a document for this source file already exists in the project, carry on. - if (currentDocuments.Remove(sourceFile)) - { - continue; - } - - // If the source file doesn't exist on disk, don't try to add it. - if (!File.Exists(sourceFile)) - { - continue; - } - - _workspace.AddDocument(project.Id, sourceFile); - } - - // Removing any remaining documents from the project. - foreach (var currentDocument in currentDocuments) - { - _workspace.RemoveDocument(currentDocument.Value); - } - } - - private void WatchDirectoryContainingFile(string sourceFile) - => _fileSystemWatcher.Watch(Path.GetDirectoryName(sourceFile), OnDirectoryFileChanged); - - private void OnDirectoryFileChanged(string path, FileChangeType changeType) - { - // Hosts may not have passed through a file change type - if (changeType == FileChangeType.Unspecified && !File.Exists(path) || changeType == FileChangeType.Delete) - { - foreach (var documentId in _workspace.CurrentSolution.GetDocumentIdsWithFilePath(path)) - { - _workspace.RemoveDocument(documentId); - } - } - - if (changeType == FileChangeType.Unspecified || changeType == FileChangeType.Create) - { - // Only add cs files. Also, make sure the path is a file, and not a directory name that happens to end in ".cs" - if (string.Equals(Path.GetExtension(path), ".cs", StringComparison.CurrentCultureIgnoreCase) && File.Exists(path)) - { - // Use the buffer manager to add the new file to the appropriate projects - // Hosts that don't pass the FileChangeType may wind up updating the buffer twice - _workspace.BufferManager.UpdateBufferAsync(new UpdateBufferRequest() { FileName = path, FromDisk = true }).Wait(); - } - } - } - - private void UpdateParseOptions(Project project, LanguageVersion languageVersion, IEnumerable preprocessorSymbolNames, bool generateXmlDocumentation) - { - var existingParseOptions = (CSharpParseOptions)project.ParseOptions; - - if (existingParseOptions.LanguageVersion == languageVersion && - Enumerable.SequenceEqual(existingParseOptions.PreprocessorSymbolNames, preprocessorSymbolNames) && - (existingParseOptions.DocumentationMode == DocumentationMode.Diagnose) == generateXmlDocumentation) - { - // No changes to make. Moving on. - return; - } - - var parseOptions = new CSharpParseOptions(languageVersion); - - if (preprocessorSymbolNames.Any()) - { - parseOptions = parseOptions.WithPreprocessorSymbols(preprocessorSymbolNames); - } - - if (generateXmlDocumentation) - { - parseOptions = parseOptions.WithDocumentationMode(DocumentationMode.Diagnose); - } - - _workspace.SetParseOptions(project.Id, parseOptions); - } - - private void UpdateProjectReferences(Project project, ImmutableArray projectReferencePaths) - { - _logger.LogInformation($"Update project: {project.Name}"); - - var existingProjectReferences = new HashSet(project.ProjectReferences); - var addedProjectReferences = new HashSet(); - - foreach (var projectReferencePath in projectReferencePaths) - { - if (!_projects.TryGetValue(projectReferencePath, out var referencedProject)) - { - if (File.Exists(projectReferencePath)) - { - _logger.LogInformation($"Found referenced project outside root directory: {projectReferencePath}"); - - // We've found a project reference that we didn't know about already, but it exists on disk. - // This is likely a project that is outside of OmniSharp's TargetDirectory. - referencedProject = LoadProject(projectReferencePath); - - if (referencedProject != null) - { - AddProject(referencedProject); - - // Ensure this project is queued to be updated later. - _projectsToProcess.Enqueue(referencedProject); - } - } - } - - if (referencedProject == null) - { - _logger.LogWarning($"Unable to resolve project reference '{projectReferencePath}' for '{project.Name}'."); - continue; - } - - var projectReference = new ProjectReference(referencedProject.Id); - - if (existingProjectReferences.Remove(projectReference)) - { - // This reference already exists - continue; - } - - if (!addedProjectReferences.Contains(projectReference)) - { - _workspace.AddProjectReference(project.Id, projectReference); - addedProjectReferences.Add(projectReference); - } - } - - foreach (var existingProjectReference in existingProjectReferences) - { - _workspace.RemoveProjectReference(project.Id, existingProjectReference); - } - } - - private class MetadataReferenceComparer : IEqualityComparer - { - public static MetadataReferenceComparer Instance { get; } = new MetadataReferenceComparer(); - - public bool Equals(MetadataReference x, MetadataReference y) - => x is PortableExecutableReference pe1 && y is PortableExecutableReference pe2 - ? StringComparer.OrdinalIgnoreCase.Equals(pe1.FilePath, pe2.FilePath) - : EqualityComparer.Default.Equals(x, y); - - public int GetHashCode(MetadataReference obj) - => obj is PortableExecutableReference pe - ? StringComparer.OrdinalIgnoreCase.GetHashCode(pe.FilePath) - : EqualityComparer.Default.GetHashCode(obj); - } - - private void UpdateReferences(Project project, ImmutableArray referencePaths) - { - var referencesToRemove = new HashSet(project.MetadataReferences, MetadataReferenceComparer.Instance); - var referencesToAdd = new HashSet(MetadataReferenceComparer.Instance); - - foreach (var referencePath in referencePaths) - { - if (!File.Exists(referencePath)) - { - _logger.LogWarning($"Unable to resolve assembly '{referencePath}'"); - } - else - { - var reference = _metadataFileReferenceCache.GetMetadataReference(referencePath); - - if (referencesToRemove.Remove(reference)) - { - continue; - } - - if (!referencesToAdd.Contains(reference)) - { - _logger.LogDebug($"Adding reference '{referencePath}' to '{project.Name}'."); - _workspace.AddMetadataReference(project.Id, reference); - referencesToAdd.Add(reference); - } - } - } - - foreach (var reference in referencesToRemove) - { - _workspace.RemoveMetadataReference(project.Id, reference); - } - } - - private void CheckForUnresolvedDependences(ProjectFileInfo projectFile, bool allowAutoRestore) - { - var unresolvedPackageReferences = _packageDepedencyResolver.FindUnresolvedPackageReferences(projectFile); - if (unresolvedPackageReferences.IsEmpty) - { - return; - } - - var unresolvedDependencies = unresolvedPackageReferences.Select(packageReference => - new PackageDependency - { - Name = packageReference.Dependency.Id, - Version = packageReference.Dependency.VersionRange.ToNormalizedString() - }); - - if (allowAutoRestore && _options.EnablePackageAutoRestore) - { - _dotNetCli.RestoreAsync(projectFile.Directory, onFailure: () => - { - _eventEmitter.UnresolvedDepdendencies(projectFile.FilePath, unresolvedDependencies); - }); - } - else - { - _eventEmitter.UnresolvedDepdendencies(projectFile.FilePath, unresolvedDependencies); - } - } - - private ProjectFileInfo GetProjectFileInfo(string path) - { - if (!_projects.TryGetValue(path, out var projectFileInfo)) - { - return null; - } - - return projectFileInfo; - } - - Task IProjectSystem.GetWorkspaceModelAsync(WorkspaceInformationRequest request) - { - var info = new MSBuildWorkspaceInfo( - _solutionFileOrRootPath, _projects, - excludeSourceFiles: request?.ExcludeSourceFiles ?? false); - - return Task.FromResult(info); - } - - Task IProjectSystem.GetProjectModelAsync(string filePath) - { - var document = _workspace.GetDocument(filePath); - - var projectFilePath = document != null - ? document.Project.FilePath - : filePath; - - var projectFileInfo = GetProjectFileInfo(projectFilePath); - if (projectFileInfo == null) - { - _logger.LogDebug($"Could not locate project for '{projectFilePath}'"); - return Task.FromResult(null); - } - - var info = new MSBuildProjectInfo(projectFileInfo); - - return Task.FromResult(info); - } - } -} diff --git a/src/OmniSharp.MSBuild/Resolution/PackageDependencyResolver.cs b/src/OmniSharp.MSBuild/PackageDependencyChecker.cs similarity index 69% rename from src/OmniSharp.MSBuild/Resolution/PackageDependencyResolver.cs rename to src/OmniSharp.MSBuild/PackageDependencyChecker.cs index 01cbc37cf3..17d7881929 100644 --- a/src/OmniSharp.MSBuild/Resolution/PackageDependencyResolver.cs +++ b/src/OmniSharp.MSBuild/PackageDependencyChecker.cs @@ -5,17 +5,55 @@ using System.Linq; using Microsoft.Extensions.Logging; using NuGet.ProjectModel; +using OmniSharp.Eventing; +using OmniSharp.Models.Events; using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Options; +using OmniSharp.Services; -namespace OmniSharp.MSBuild.Resolution +namespace OmniSharp.MSBuild { - internal class PackageDependencyResolver + internal class PackageDependencyChecker { private readonly ILogger _logger; + private readonly IEventEmitter _eventEmitter; + private readonly DotNetCliService _dotNetCli; + private readonly MSBuildOptions _options; - public PackageDependencyResolver(ILoggerFactory loggerFactory) + public PackageDependencyChecker(ILoggerFactory loggerFactory, IEventEmitter eventEmitter, DotNetCliService dotNetCli, MSBuildOptions options) { - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); + _eventEmitter = eventEmitter; + _dotNetCli = dotNetCli; + _options = options; + } + + public void CheckForUnresolvedDependences(ProjectFileInfo projectFile, bool allowAutoRestore) + { + var unresolvedPackageReferences = FindUnresolvedPackageReferences(projectFile); + if (unresolvedPackageReferences.IsEmpty) + { + return; + } + + var unresolvedDependencies = unresolvedPackageReferences.Select(packageReference => + new PackageDependency + { + Name = packageReference.Dependency.Id, + Version = packageReference.Dependency.VersionRange.ToNormalizedString() + }); + + if (allowAutoRestore && _options.EnablePackageAutoRestore) + { + _dotNetCli.RestoreAsync(projectFile.Directory, onFailure: () => + { + _eventEmitter.UnresolvedDepdendencies(projectFile.FilePath, unresolvedDependencies); + }); + } + else + { + _eventEmitter.UnresolvedDepdendencies(projectFile.FilePath, unresolvedDependencies); + } } public ImmutableArray FindUnresolvedPackageReferences(ProjectFileInfo projectFile) diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs index 61f3dae3c5..7117155e99 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs @@ -4,11 +4,12 @@ using System.IO; using System.Linq; using System.Runtime.Versioning; -using Microsoft.Build.Execution; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using NuGet.Packaging.Core; +using MSB = Microsoft.Build; + namespace OmniSharp.MSBuild.ProjectFile { internal partial class ProjectFileInfo @@ -54,12 +55,7 @@ private ProjectData( ImmutableArray preprocessorSymbolNames, ImmutableArray suppressedDiagnosticIds, bool signAssembly, - string assemblyOriginatorKeyFile, - ImmutableArray sourceFiles, - ImmutableArray projectReferences, - ImmutableArray references, - ImmutableArray packageReferences, - ImmutableArray analyzers) + string assemblyOriginatorKeyFile) { Guid = guid; Name = name; @@ -81,7 +77,30 @@ private ProjectData( SignAssembly = signAssembly; AssemblyOriginatorKeyFile = assemblyOriginatorKeyFile; + } + private ProjectData( + Guid guid, string name, + string assemblyName, string targetPath, string outputPath, string projectAssetsFile, + FrameworkName targetFramework, + ImmutableArray targetFrameworks, + OutputKind outputKind, + LanguageVersion languageVersion, + bool allowUnsafeCode, + string documentationFile, + ImmutableArray preprocessorSymbolNames, + ImmutableArray suppressedDiagnosticIds, + bool signAssembly, + string assemblyOriginatorKeyFile, + ImmutableArray sourceFiles, + ImmutableArray projectReferences, + ImmutableArray references, + ImmutableArray packageReferences, + ImmutableArray analyzers) + : this(guid, name, assemblyName, targetPath, outputPath, projectAssetsFile, + targetFramework, targetFrameworks, outputKind, languageVersion, allowUnsafeCode, + documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile) + { SourceFiles = sourceFiles; ProjectReferences = projectReferences; References = references; @@ -89,7 +108,41 @@ private ProjectData( Analyzers = analyzers; } - public static ProjectData Create(ProjectInstance projectInstance) + public static ProjectData Create(MSB.Evaluation.Project project) + { + var guid = PropertyConverter.ToGuid(project.GetPropertyValue(PropertyNames.ProjectGuid)); + var name = project.GetPropertyValue(PropertyNames.ProjectName); + var assemblyName = project.GetPropertyValue(PropertyNames.AssemblyName); + var targetPath = project.GetPropertyValue(PropertyNames.TargetPath); + var outputPath = project.GetPropertyValue(PropertyNames.OutputPath); + var projectAssetsFile = project.GetPropertyValue(PropertyNames.ProjectAssetsFile); + + var targetFramework = new FrameworkName(project.GetPropertyValue(PropertyNames.TargetFrameworkMoniker)); + + var targetFrameworkValue = project.GetPropertyValue(PropertyNames.TargetFramework); + var targetFrameworks = PropertyConverter.SplitList(project.GetPropertyValue(PropertyNames.TargetFrameworks), ';'); + + if (!string.IsNullOrWhiteSpace(targetFrameworkValue) && targetFrameworks.Length == 0) + { + targetFrameworks = ImmutableArray.Create(targetFrameworkValue); + } + + var languageVersion = PropertyConverter.ToLanguageVersion(project.GetPropertyValue(PropertyNames.LangVersion)); + var allowUnsafeCode = PropertyConverter.ToBoolean(project.GetPropertyValue(PropertyNames.AllowUnsafeBlocks), defaultValue: false); + var outputKind = PropertyConverter.ToOutputKind(project.GetPropertyValue(PropertyNames.OutputType)); + var documentationFile = project.GetPropertyValue(PropertyNames.DocumentationFile); + var preprocessorSymbolNames = PropertyConverter.ToPreprocessorSymbolNames(project.GetPropertyValue(PropertyNames.DefineConstants)); + var suppressedDiagnosticIds = PropertyConverter.ToSuppressedDiagnosticIds(project.GetPropertyValue(PropertyNames.NoWarn)); + var signAssembly = PropertyConverter.ToBoolean(project.GetPropertyValue(PropertyNames.SignAssembly), defaultValue: false); + var assemblyOriginatorKeyFile = project.GetPropertyValue(PropertyNames.AssemblyOriginatorKeyFile); + + return new ProjectData( + guid, name, assemblyName, targetPath, outputPath, projectAssetsFile, + targetFramework, targetFrameworks, outputKind, languageVersion, allowUnsafeCode, + documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile); + } + + public static ProjectData Create(MSB.Execution.ProjectInstance projectInstance) { var guid = PropertyConverter.ToGuid(projectInstance.GetPropertyValue(PropertyNames.ProjectGuid)); var name = projectInstance.GetPropertyValue(PropertyNames.ProjectName); @@ -113,7 +166,7 @@ public static ProjectData Create(ProjectInstance projectInstance) var outputKind = PropertyConverter.ToOutputKind(projectInstance.GetPropertyValue(PropertyNames.OutputType)); var documentationFile = projectInstance.GetPropertyValue(PropertyNames.DocumentationFile); var preprocessorSymbolNames = PropertyConverter.ToPreprocessorSymbolNames(projectInstance.GetPropertyValue(PropertyNames.DefineConstants)); - var suppressDiagnosticIds = PropertyConverter.ToSuppressDiagnosticIds(projectInstance.GetPropertyValue(PropertyNames.NoWarn)); + var suppressedDiagnosticIds = PropertyConverter.ToSuppressedDiagnosticIds(projectInstance.GetPropertyValue(PropertyNames.NoWarn)); var signAssembly = PropertyConverter.ToBoolean(projectInstance.GetPropertyValue(PropertyNames.SignAssembly), defaultValue: false); var assemblyOriginatorKeyFile = projectInstance.GetPropertyValue(PropertyNames.AssemblyOriginatorKeyFile); @@ -128,18 +181,18 @@ public static ProjectData Create(ProjectInstance projectInstance) return new ProjectData(guid, name, assemblyName, targetPath, outputPath, projectAssetsFile, targetFramework, targetFrameworks, - outputKind, languageVersion, allowUnsafeCode, documentationFile, preprocessorSymbolNames, suppressDiagnosticIds, + outputKind, languageVersion, allowUnsafeCode, documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile, sourceFiles, projectReferences, references, packageReferences, analyzers); } - private static bool ReferenceSourceTargetIsNotProjectReference(ProjectItemInstance item) + private static bool ReferenceSourceTargetIsNotProjectReference(MSB.Execution.ProjectItemInstance item) => item.GetMetadataValue(MetadataNames.ReferenceSourceTarget) != ItemNames.ProjectReference; private static bool FileNameIsNotGenerated(string filePath) => !Path.GetFileName(filePath).StartsWith("TemporaryGeneratedFile_"); - private static ImmutableArray GetFullPaths(IEnumerable items, Func filter = null) + private static ImmutableArray GetFullPaths(IEnumerable items, Func filter = null) { var builder = ImmutableArray.CreateBuilder(); var addedSet = new HashSet(); @@ -159,7 +212,7 @@ private static ImmutableArray GetFullPaths(IEnumerable GetPackageReferences(ICollection items) + private static ImmutableArray GetPackageReferences(ICollection items) { var builder = ImmutableArray.CreateBuilder(items.Count); var addedSet = new HashSet(); diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs index 13265a9c7a..bf30719a01 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs @@ -46,11 +46,6 @@ internal partial class ProjectFileInfo public ImmutableArray PackageReferences => _data.PackageReferences; public ImmutableArray Analyzers => _data.Analyzers; - internal ProjectFileInfo(string filePath) - { - this.FilePath = filePath; - } - private ProjectFileInfo( ProjectId id, string filePath, @@ -63,7 +58,23 @@ private ProjectFileInfo( _data = data; } - public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Create(string filePath, ProjectLoader loader) + internal static ProjectFileInfo CreateEmpty(string filePath) + { + var id = ProjectId.CreateNewId(debugName: filePath); + + return new ProjectFileInfo(id, filePath, data: null); + } + + internal static ProjectFileInfo CreateNoBuild(string filePath, ProjectLoader loader) + { + var id = ProjectId.CreateNewId(debugName: filePath); + var project = loader.EvaluateProjectFile(filePath); + var data = ProjectData.Create(project); + + return new ProjectFileInfo(id, filePath, data); + } + + public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Load(string filePath, ProjectLoader loader) { if (!File.Exists(filePath)) { diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs index 36db94057e..ff186a0029 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs @@ -1,10 +1,9 @@ using System; -using System.Collections; using System.Collections.Generic; namespace OmniSharp.MSBuild.ProjectFile { - internal class ProjectFileInfoCollection : IEnumerable + internal class ProjectFileInfoCollection { private readonly List _items; private readonly Dictionary _itemMap; @@ -15,18 +14,7 @@ public ProjectFileInfoCollection() _itemMap = new Dictionary(StringComparer.OrdinalIgnoreCase); } - public IEnumerator GetEnumerator() - { - foreach (var item in _items) - { - yield return item; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + public IEnumerable GetItems() => _items.ToArray(); public void Add(ProjectFileInfo fileInfo) { @@ -49,6 +37,18 @@ public bool ContainsKey(string filePath) return _itemMap.ContainsKey(filePath); } + public bool Remove(string filePath) + { + if (_itemMap.TryGetValue(filePath, out var fileInfo)) + { + _items.Remove(fileInfo); + _itemMap.Remove(filePath); + return true; + } + + return false; + } + public bool TryGetValue(string filePath, out ProjectFileInfo fileInfo) { return _itemMap.TryGetValue(filePath, out fileInfo); diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs new file mode 100644 index 0000000000..30aca47135 --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace OmniSharp.MSBuild.ProjectFile +{ + internal static class ProjectFileInfoExtensions + { + public static CSharpCompilationOptions CreateCompilationOptions(this ProjectFileInfo projectFileInfo) + { + var result = new CSharpCompilationOptions(projectFileInfo.OutputKind); + + result = result.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default); + + if (projectFileInfo.AllowUnsafeCode) + { + result = result.WithAllowUnsafe(true); + } + + var specificDiagnosticOptions = new Dictionary(projectFileInfo.SuppressedDiagnosticIds.Count) + { + // Ensure that specific warnings about assembly references are always suppressed. + { "CS1701", ReportDiagnostic.Suppress }, + { "CS1702", ReportDiagnostic.Suppress }, + { "CS1705", ReportDiagnostic.Suppress } + }; + + if (projectFileInfo.SuppressedDiagnosticIds.Any()) + { + foreach (var id in projectFileInfo.SuppressedDiagnosticIds) + { + if (!specificDiagnosticOptions.ContainsKey(id)) + { + specificDiagnosticOptions.Add(id, ReportDiagnostic.Suppress); + } + } + } + + result = result.WithSpecificDiagnosticOptions(specificDiagnosticOptions); + + if (projectFileInfo.SignAssembly && !string.IsNullOrEmpty(projectFileInfo.AssemblyOriginatorKeyFile)) + { + var keyFile = Path.Combine(projectFileInfo.Directory, projectFileInfo.AssemblyOriginatorKeyFile); + result = result.WithStrongNameProvider(new DesktopStrongNameProvider()) + .WithCryptoKeyFile(keyFile); + } + + if (!string.IsNullOrWhiteSpace(projectFileInfo.DocumentationFile)) + { + result = result.WithXmlReferenceResolver(XmlFileResolver.Default); + } + + return result; + } + + public static ProjectInfo CreateProjectInfo(this ProjectFileInfo projectFileInfo) + { + return ProjectInfo.Create( + id: projectFileInfo.Id, + version: VersionStamp.Create(), + name: projectFileInfo.Name, + assemblyName: projectFileInfo.AssemblyName, + language: LanguageNames.CSharp, + filePath: projectFileInfo.FilePath, + outputFilePath: projectFileInfo.TargetPath, + compilationOptions: projectFileInfo.CreateCompilationOptions()); + } + } +} diff --git a/src/OmniSharp.MSBuild/ProjectFile/PropertyConverter.cs b/src/OmniSharp.MSBuild/ProjectFile/PropertyConverter.cs index f856e4dddc..0fa9165154 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/PropertyConverter.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/PropertyConverter.cs @@ -80,7 +80,7 @@ public static ImmutableArray ToPreprocessorSymbolNames(string propertyVa return ImmutableArray.CreateRange(values); } - public static ImmutableArray ToSuppressDiagnosticIds(string propertyValue) + public static ImmutableArray ToSuppressedDiagnosticIds(string propertyValue) { if (string.IsNullOrWhiteSpace(propertyValue)) { diff --git a/src/OmniSharp.MSBuild/ProjectLoader.cs b/src/OmniSharp.MSBuild/ProjectLoader.cs index 87995f3f89..121a554929 100644 --- a/src/OmniSharp.MSBuild/ProjectLoader.cs +++ b/src/OmniSharp.MSBuild/ProjectLoader.cs @@ -15,24 +15,14 @@ internal class ProjectLoader { private readonly ILogger _logger; private readonly Dictionary _globalProperties; - private readonly MSB.Evaluation.ProjectCollection _projectCollection; - private readonly string _toolsVersion; + private readonly MSBuildOptions _options; public ProjectLoader(MSBuildOptions options, string solutionDirectory, ImmutableDictionary propertyOverrides, ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); - options = options ?? new MSBuildOptions(); + _options = options ?? new MSBuildOptions(); - _globalProperties = CreateGlobalProperties(options, solutionDirectory, propertyOverrides, _logger); - _projectCollection = new MSB.Evaluation.ProjectCollection(_globalProperties); - - var toolsVersion = options.ToolsVersion; - if (string.IsNullOrEmpty(toolsVersion) || Version.TryParse(toolsVersion, out _)) - { - toolsVersion = _projectCollection.DefaultToolsVersion; - } - - _toolsVersion = GetLegalToolsetVersion(toolsVersion, _projectCollection.Toolsets); + _globalProperties = CreateGlobalProperties(_options, solutionDirectory, propertyOverrides, _logger); } private static Dictionary CreateGlobalProperties( @@ -66,8 +56,7 @@ private static Dictionary CreateGlobalProperties( public (MSB.Execution.ProjectInstance projectInstance, ImmutableArray diagnostics) BuildProject(string filePath) { - // Evaluate the MSBuild project - var evaluatedProject = _projectCollection.LoadProject(filePath, _toolsVersion); + var evaluatedProject = EvaluateProjectFile(filePath); SetTargetFrameworkIfNeeded(evaluatedProject); @@ -84,6 +73,22 @@ private static Dictionary CreateGlobalProperties( : (null, diagnostics); } + public MSB.Evaluation.Project EvaluateProjectFile(string filePath) + { + // Evaluate the MSBuild project + var projectCollection = new MSB.Evaluation.ProjectCollection(_globalProperties); + + var toolsVersion = _options.ToolsVersion; + if (string.IsNullOrEmpty(toolsVersion) || Version.TryParse(toolsVersion, out _)) + { + toolsVersion = projectCollection.DefaultToolsVersion; + } + + toolsVersion = GetLegalToolsetVersion(toolsVersion, projectCollection.Toolsets); + + return projectCollection.LoadProject(filePath, toolsVersion); + } + private static void SetTargetFrameworkIfNeeded(MSB.Evaluation.Project evaluatedProject) { var targetFramework = evaluatedProject.GetPropertyValue(PropertyNames.TargetFramework); diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs new file mode 100644 index 0000000000..766a42729e --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.Logging; +using OmniSharp.Eventing; +using OmniSharp.FileWatching; +using OmniSharp.Models.UpdateBuffer; +using OmniSharp.MSBuild.Logging; +using OmniSharp.MSBuild.Models.Events; +using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Services; +using OmniSharp.Utilities; + +namespace OmniSharp.MSBuild +{ + internal class ProjectManager : DisposableObject + { + private readonly ILogger _logger; + private readonly IEventEmitter _eventEmitter; + private readonly IFileSystemWatcher _fileSystemWatcher; + private readonly MetadataFileReferenceCache _metadataFileReferenceCache; + private readonly PackageDependencyChecker _packageDependencyChecker; + private readonly ProjectFileInfoCollection _projectFiles; + private readonly ProjectLoader _projectLoader; + private readonly OmniSharpWorkspace _workspace; + + private const int LoopDelay = 100; // milliseconds + private readonly BufferBlock _queue; + private readonly CancellationTokenSource _processLoopCancellation; + private readonly Task _processLoopTask; + private bool _processingQueue; + + public ProjectManager(ILoggerFactory loggerFactory, IEventEmitter eventEmitter, IFileSystemWatcher fileSystemWatcher, MetadataFileReferenceCache metadataFileReferenceCache, PackageDependencyChecker packageDependencyChecker, ProjectLoader projectLoader, OmniSharpWorkspace workspace) + { + _logger = loggerFactory.CreateLogger(); + _eventEmitter = eventEmitter; + _fileSystemWatcher = fileSystemWatcher; + _metadataFileReferenceCache = metadataFileReferenceCache; + _packageDependencyChecker = packageDependencyChecker; + _projectFiles = new ProjectFileInfoCollection(); + _projectLoader = projectLoader; + _workspace = workspace; + + _queue = new BufferBlock(); + _processLoopCancellation = new CancellationTokenSource(); + _processLoopTask = Task.Run(() => ProcessLoopAsync(_processLoopCancellation.Token)); + } + + protected override void DisposeCore(bool disposing) + { + if (IsDisposed) + { + return; + } + + _processLoopCancellation.Cancel(); + _processLoopCancellation.Dispose(); + } + + public IEnumerable GetAllProjects() => _projectFiles.GetItems(); + public bool TryGetProject(string projectFilePath, out ProjectFileInfo projectFileInfo) => _projectFiles.TryGetValue(projectFilePath, out projectFileInfo); + + public void QueueProjectUpdate(string projectFilePath) + { + _logger.LogInformation($"Queue project update for '{projectFilePath}'"); + _queue.Post(projectFilePath); + } + + public async Task WaitForQueueEmptyAsync() + { + while (_queue.Count > 0 || _processingQueue) + { + await Task.Delay(LoopDelay); + } + } + + private async Task ProcessLoopAsync(CancellationToken cancellationToken) + { + while (true) + { + await Task.Delay(LoopDelay, cancellationToken); + ProcessQueue(cancellationToken); + } + } + + private void ProcessQueue(CancellationToken cancellationToken) + { + _processingQueue = true; + try + { + HashSet processedSet = null; + + while (_queue.TryReceive(out var projectFilePath)) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + if (processedSet == null) + { + processedSet = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + // Ensure that we don't process the same project twice. + if (!processedSet.Add(projectFilePath)) + { + continue; + } + + // TODO: Handle removing project + + // update or add project + if (_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + { + projectFileInfo = ReloadProject(projectFileInfo); + _projectFiles[projectFilePath] = projectFileInfo; + } + else + { + projectFileInfo = LoadProject(projectFilePath); + AddProject(projectFileInfo); + } + } + + if (processedSet != null) + { + foreach (var projectFilePath in processedSet) + { + UpdateProject(projectFilePath); + } + + foreach (var projectFilePath in processedSet) + { + if (_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + { + _packageDependencyChecker.CheckForUnresolvedDependences(projectFileInfo, allowAutoRestore: true); + } + } + } + } + finally + { + _processingQueue = false; + } + } + + private ProjectFileInfo LoadProject(string projectFilePath) + => LoadOrReloadProject(projectFilePath, () => ProjectFileInfo.Load(projectFilePath, _projectLoader)); + + private ProjectFileInfo ReloadProject(ProjectFileInfo projectFileInfo) + => LoadOrReloadProject(projectFileInfo.FilePath, () => projectFileInfo.Reload(_projectLoader)); + + private ProjectFileInfo LoadOrReloadProject(string projectFilePath, Func<(ProjectFileInfo, ImmutableArray)> loadFunc) + { + _logger.LogInformation($"Loading project: {projectFilePath}"); + + ProjectFileInfo projectFileInfo; + ImmutableArray diagnostics; + + try + { + (projectFileInfo, diagnostics) = loadFunc(); + + if (projectFileInfo == null) + { + _logger.LogWarning($"Failed to load project file '{projectFilePath}'."); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to load project file '{projectFilePath}'.", ex); + _eventEmitter.Error(ex, fileName: projectFilePath); + projectFileInfo = null; + } + + _eventEmitter.MSBuildProjectDiagnostics(projectFilePath, diagnostics); + + return projectFileInfo; + } + + private bool RemoveProject(string projectFilePath) + { + if (!_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + { + return false; + } + + _projectFiles.Remove(projectFilePath); + + var newSolution = _workspace.CurrentSolution.RemoveProject(projectFileInfo.Id); + + if (!_workspace.TryApplyChanges(newSolution)) + { + _logger.LogError($"Failed to remove project from workspace: '{projectFileInfo.FilePath}'"); + } + + // TODO: Stop watching project files + + return true; + } + + private void AddProject(ProjectFileInfo projectFileInfo) + { + _logger.LogInformation($"Adding project '{projectFileInfo.FilePath}'"); + + _projectFiles.Add(projectFileInfo); + + var projectInfo = projectFileInfo.CreateProjectInfo(); + var newSolution = _workspace.CurrentSolution.AddProject(projectInfo); + + if (!_workspace.TryApplyChanges(newSolution)) + { + _logger.LogError($"Failed to add project to workspace: '{projectFileInfo.FilePath}'"); + } + + WatchProjectFiles(projectFileInfo); + } + + private void WatchProjectFiles(ProjectFileInfo projectFileInfo) + { + // TODO: This needs some improvement. Currently, it tracks both deletions and changes + // as "updates". We should properly remove projects that are deleted. + _fileSystemWatcher.Watch(projectFileInfo.FilePath, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + + if (!string.IsNullOrEmpty(projectFileInfo.ProjectAssetsFile)) + { + _fileSystemWatcher.Watch(projectFileInfo.ProjectAssetsFile, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + + var restoreDirectory = Path.GetDirectoryName(projectFileInfo.ProjectAssetsFile); + var nugetFileBase = Path.Combine(restoreDirectory, Path.GetFileName(projectFileInfo.FilePath) + ".nuget"); + var nugetCacheFile = nugetFileBase + ".cache"; + var nugetPropsFile = nugetFileBase + ".g.props"; + var nugetTargetsFile = nugetFileBase + ".g.targets"; + + _fileSystemWatcher.Watch(nugetCacheFile, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + + _fileSystemWatcher.Watch(nugetPropsFile, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + + _fileSystemWatcher.Watch(nugetTargetsFile, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + } + } + + private void UpdateProject(string projectFilePath) + { + if (!_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + { + _logger.LogError($"Attemped to update project that is not loaded: {projectFilePath}"); + return; + } + + var project = _workspace.CurrentSolution.GetProject(projectFileInfo.Id); + if (project == null) + { + _logger.LogError($"Could not locate project in workspace: {projectFileInfo.FilePath}"); + return; + } + + UpdateSourceFiles(project, projectFileInfo.SourceFiles); + UpdateParseOptions(project, projectFileInfo.LanguageVersion, projectFileInfo.PreprocessorSymbolNames, !string.IsNullOrWhiteSpace(projectFileInfo.DocumentationFile)); + UpdateProjectReferences(project, projectFileInfo.ProjectReferences); + UpdateReferences(project, projectFileInfo.References); + } + + private void UpdateSourceFiles(Project project, IList sourceFiles) + { + var currentDocuments = project.Documents.ToDictionary(d => d.FilePath, d => d.Id); + + // Add source files to the project. + foreach (var sourceFile in sourceFiles) + { + _fileSystemWatcher.Watch(Path.GetDirectoryName(sourceFile), OnDirectoryFileChanged); + + // If a document for this source file already exists in the project, carry on. + if (currentDocuments.Remove(sourceFile)) + { + continue; + } + + // If the source file doesn't exist on disk, don't try to add it. + if (!File.Exists(sourceFile)) + { + continue; + } + + _workspace.AddDocument(project.Id, sourceFile); + } + + // Removing any remaining documents from the project. + foreach (var currentDocument in currentDocuments) + { + _workspace.RemoveDocument(currentDocument.Value); + } + } + + private void OnDirectoryFileChanged(string path, FileChangeType changeType) + { + // Hosts may not have passed through a file change type + if (changeType == FileChangeType.Unspecified && !File.Exists(path) || changeType == FileChangeType.Delete) + { + foreach (var documentId in _workspace.CurrentSolution.GetDocumentIdsWithFilePath(path)) + { + _workspace.RemoveDocument(documentId); + } + } + + if (changeType == FileChangeType.Unspecified || changeType == FileChangeType.Create) + { + // Only add cs files. Also, make sure the path is a file, and not a directory name that happens to end in ".cs" + if (string.Equals(Path.GetExtension(path), ".cs", StringComparison.CurrentCultureIgnoreCase) && File.Exists(path)) + { + // Use the buffer manager to add the new file to the appropriate projects + // Hosts that don't pass the FileChangeType may wind up updating the buffer twice + _workspace.BufferManager.UpdateBufferAsync(new UpdateBufferRequest() { FileName = path, FromDisk = true }).Wait(); + } + } + } + + private void UpdateParseOptions(Project project, LanguageVersion languageVersion, IEnumerable preprocessorSymbolNames, bool generateXmlDocumentation) + { + var existingParseOptions = (CSharpParseOptions)project.ParseOptions; + + if (existingParseOptions.LanguageVersion == languageVersion && + Enumerable.SequenceEqual(existingParseOptions.PreprocessorSymbolNames, preprocessorSymbolNames) && + (existingParseOptions.DocumentationMode == DocumentationMode.Diagnose) == generateXmlDocumentation) + { + // No changes to make. Moving on. + return; + } + + var parseOptions = new CSharpParseOptions(languageVersion); + + if (preprocessorSymbolNames.Any()) + { + parseOptions = parseOptions.WithPreprocessorSymbols(preprocessorSymbolNames); + } + + if (generateXmlDocumentation) + { + parseOptions = parseOptions.WithDocumentationMode(DocumentationMode.Diagnose); + } + + _workspace.SetParseOptions(project.Id, parseOptions); + } + + private void UpdateProjectReferences(Project project, ImmutableArray projectReferencePaths) + { + _logger.LogInformation($"Update project: {project.Name}"); + + var existingProjectReferences = new HashSet(project.ProjectReferences); + var addedProjectReferences = new HashSet(); + + foreach (var projectReferencePath in projectReferencePaths) + { + if (!_projectFiles.TryGetValue(projectReferencePath, out var referencedProject)) + { + if (File.Exists(projectReferencePath)) + { + _logger.LogInformation($"Found referenced project outside root directory: {projectReferencePath}"); + + // We've found a project reference that we didn't know about already, but it exists on disk. + // This is likely a project that is outside of OmniSharp's TargetDirectory. + referencedProject = ProjectFileInfo.CreateNoBuild(projectReferencePath, _projectLoader); + AddProject(referencedProject); + + QueueProjectUpdate(projectReferencePath); + } + } + + if (referencedProject == null) + { + _logger.LogWarning($"Unable to resolve project reference '{projectReferencePath}' for '{project.Name}'."); + continue; + } + + var projectReference = new ProjectReference(referencedProject.Id); + + if (existingProjectReferences.Remove(projectReference)) + { + // This reference already exists + continue; + } + + if (!addedProjectReferences.Contains(projectReference)) + { + _workspace.AddProjectReference(project.Id, projectReference); + addedProjectReferences.Add(projectReference); + } + } + + foreach (var existingProjectReference in existingProjectReferences) + { + _workspace.RemoveProjectReference(project.Id, existingProjectReference); + } + } + + private class MetadataReferenceComparer : IEqualityComparer + { + public static MetadataReferenceComparer Instance { get; } = new MetadataReferenceComparer(); + + public bool Equals(MetadataReference x, MetadataReference y) + => x is PortableExecutableReference pe1 && y is PortableExecutableReference pe2 + ? StringComparer.OrdinalIgnoreCase.Equals(pe1.FilePath, pe2.FilePath) + : EqualityComparer.Default.Equals(x, y); + + public int GetHashCode(MetadataReference obj) + => obj is PortableExecutableReference pe + ? StringComparer.OrdinalIgnoreCase.GetHashCode(pe.FilePath) + : EqualityComparer.Default.GetHashCode(obj); + } + + private void UpdateReferences(Project project, ImmutableArray referencePaths) + { + var referencesToRemove = new HashSet(project.MetadataReferences, MetadataReferenceComparer.Instance); + var referencesToAdd = new HashSet(MetadataReferenceComparer.Instance); + + foreach (var referencePath in referencePaths) + { + if (!File.Exists(referencePath)) + { + _logger.LogWarning($"Unable to resolve assembly '{referencePath}'"); + } + else + { + var reference = _metadataFileReferenceCache.GetMetadataReference(referencePath); + + if (referencesToRemove.Remove(reference)) + { + continue; + } + + if (!referencesToAdd.Contains(reference)) + { + _logger.LogDebug($"Adding reference '{referencePath}' to '{project.Name}'."); + _workspace.AddMetadataReference(project.Id, reference); + referencesToAdd.Add(reference); + } + } + } + + foreach (var reference in referencesToRemove) + { + _workspace.RemoveMetadataReference(project.Id, reference); + } + } + } +} diff --git a/src/OmniSharp.MSBuild/ProjectSystem.cs b/src/OmniSharp.MSBuild/ProjectSystem.cs new file mode 100644 index 0000000000..28b85770cb --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectSystem.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.IO; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OmniSharp.Eventing; +using OmniSharp.FileWatching; +using OmniSharp.Models.WorkspaceInformation; +using OmniSharp.MSBuild.Discovery; +using OmniSharp.MSBuild.Models; +using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.MSBuild.SolutionParsing; +using OmniSharp.Options; +using OmniSharp.Services; + +namespace OmniSharp.MSBuild +{ + [Export(typeof(IProjectSystem)), Shared] + public class ProjectSystem : IProjectSystem + { + private readonly IOmniSharpEnvironment _environment; + private readonly OmniSharpWorkspace _workspace; + private readonly ImmutableDictionary _propertyOverrides; + private readonly DotNetCliService _dotNetCli; + private readonly MetadataFileReferenceCache _metadataFileReferenceCache; + private readonly IEventEmitter _eventEmitter; + private readonly IFileSystemWatcher _fileSystemWatcher; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + private readonly object _gate = new object(); + private readonly Queue _projectsToProcess; + + private PackageDependencyChecker _packageDependencyChecker; + private ProjectManager _manager; + private ProjectLoader _loader; + private MSBuildOptions _options; + private string _solutionFileOrRootPath; + + public string Key { get; } = "MsBuild"; + public string Language { get; } = LanguageNames.CSharp; + public IEnumerable Extensions { get; } = new[] { ".cs" }; + + [ImportingConstructor] + public ProjectSystem( + IOmniSharpEnvironment environment, + OmniSharpWorkspace workspace, + IMSBuildLocator msbuildLocator, + DotNetCliService dotNetCliService, + MetadataFileReferenceCache metadataFileReferenceCache, + IEventEmitter eventEmitter, + IFileSystemWatcher fileSystemWatcher, + ILoggerFactory loggerFactory) + { + _environment = environment; + _workspace = workspace; + _propertyOverrides = msbuildLocator.RegisteredInstance.PropertyOverrides; + _dotNetCli = dotNetCliService; + _metadataFileReferenceCache = metadataFileReferenceCache; + _eventEmitter = eventEmitter; + _fileSystemWatcher = fileSystemWatcher; + _loggerFactory = loggerFactory; + + _projectsToProcess = new Queue(); + _logger = loggerFactory.CreateLogger(); + } + + public void Initalize(IConfiguration configuration) + { + _options = new MSBuildOptions(); + ConfigurationBinder.Bind(configuration, _options); + + if (_environment.LogLevel < LogLevel.Information) + { + var buildEnvironmentInfo = MSBuildHelpers.GetBuildEnvironmentInfo(); + _logger.LogDebug($"MSBuild environment: {Environment.NewLine}{buildEnvironmentInfo}"); + } + + _packageDependencyChecker = new PackageDependencyChecker(_loggerFactory, _eventEmitter, _dotNetCli, _options); + _loader = new ProjectLoader(_options, _environment.TargetDirectory, _propertyOverrides, _loggerFactory); + _manager = new ProjectManager(_loggerFactory, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, _loader, _workspace); + + var initialProjectPaths = GetInitialProjectPaths(); + + foreach (var projectFilePath in initialProjectPaths) + { + if (!File.Exists(projectFilePath)) + { + _logger.LogWarning($"Found project that doesn't exist on disk: {projectFilePath}"); + continue; + } + + _manager.QueueProjectUpdate(projectFilePath); + } + } + + private IEnumerable GetInitialProjectPaths() + { + // If a solution was provided, use it. + if (!string.IsNullOrEmpty(_environment.SolutionFilePath)) + { + _solutionFileOrRootPath = _environment.SolutionFilePath; + return GetProjectPathsFromSolution(_environment.SolutionFilePath); + } + + // Otherwise, assume that the path provided is a directory and look for a solution there. + var solutionFilePath = FindSolutionFilePath(_environment.TargetDirectory, _logger); + if (!string.IsNullOrEmpty(solutionFilePath)) + { + _solutionFileOrRootPath = solutionFilePath; + return GetProjectPathsFromSolution(solutionFilePath); + } + + // Finally, if there isn't a single solution immediately available, + // Just process all of the projects beneath the root path. + _solutionFileOrRootPath = _environment.TargetDirectory; + return Directory.GetFiles(_environment.TargetDirectory, "*.csproj", SearchOption.AllDirectories); + } + + private IEnumerable GetProjectPathsFromSolution(string solutionFilePath) + { + _logger.LogInformation($"Detecting projects in '{solutionFilePath}'."); + + var solutionFile = SolutionFile.ParseFile(solutionFilePath); + var processedProjects = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + + foreach (var project in solutionFile.Projects) + { + if (project.IsSolutionFolder) + { + continue; + } + + // Solution files are assumed to contain relative paths to project files with Windows-style slashes. + var projectFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar); + projectFilePath = Path.Combine(_environment.TargetDirectory, projectFilePath); + projectFilePath = Path.GetFullPath(projectFilePath); + + // Have we seen this project? If so, move on. + if (processedProjects.Contains(projectFilePath)) + { + continue; + } + + if (string.Equals(Path.GetExtension(projectFilePath), ".csproj", StringComparison.OrdinalIgnoreCase)) + { + result.Add(projectFilePath); + } + + processedProjects.Add(projectFilePath); + } + + return result; + } + + private static string FindSolutionFilePath(string rootPath, ILogger logger) + { + var solutionsFilePaths = Directory.GetFiles(rootPath, "*.sln"); + var result = SolutionSelector.Pick(solutionsFilePaths, rootPath); + + if (result.Message != null) + { + logger.LogInformation(result.Message); + } + + return result.FilePath; + } + + async Task IProjectSystem.GetWorkspaceModelAsync(WorkspaceInformationRequest request) + { + await _manager.WaitForQueueEmptyAsync(); + + return new MSBuildWorkspaceInfo( + _solutionFileOrRootPath, _manager.GetAllProjects(), + excludeSourceFiles: request?.ExcludeSourceFiles ?? false); + } + + async Task IProjectSystem.GetProjectModelAsync(string filePath) + { + await _manager.WaitForQueueEmptyAsync(); + + var document = _workspace.GetDocument(filePath); + + var projectFilePath = document != null + ? document.Project.FilePath + : filePath; + + if (!_manager.TryGetProject(projectFilePath, out var projectFileInfo)) + { + _logger.LogDebug($"Could not locate project for '{projectFilePath}'"); + return Task.FromResult(null); + } + + return new MSBuildProjectInfo(projectFileInfo); + } + } +} diff --git a/tests/OmniSharp.MSBuild.Tests/MSBuildProjectSystemTests.cs b/tests/OmniSharp.MSBuild.Tests/MSBuildProjectSystemTests.cs index 523390886c..b2326929af 100644 --- a/tests/OmniSharp.MSBuild.Tests/MSBuildProjectSystemTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/MSBuildProjectSystemTests.cs @@ -11,13 +11,11 @@ public void Project_path_is_case_insensitive() var projectPath = @"c:\projects\project1\project.csproj"; var searchProjectPath = @"c:\Projects\Project1\Project.csproj"; - var collection = new ProjectFileInfoCollection - { - new ProjectFileInfo(projectPath) - }; + var collection = new ProjectFileInfoCollection(); + collection.Add(ProjectFileInfo.CreateEmpty(projectPath)); Assert.True(collection.TryGetValue(searchProjectPath, out var outInfo)); Assert.NotNull(outInfo); } } -} \ No newline at end of file +} diff --git a/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs b/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs index 3e991bbf74..cb0710e77b 100644 --- a/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs @@ -29,7 +29,7 @@ private ProjectFileInfo CreateProjectFileInfo(OmniSharpTestHost host, ITestProje propertyOverrides: msbuildLocator.RegisteredInstance.PropertyOverrides, loggerFactory: LoggerFactory); - var (projectFileInfo, _) = ProjectFileInfo.Create(projectFilePath, loader); + var (projectFileInfo, _) = ProjectFileInfo.Load(projectFilePath, loader); return projectFileInfo; } diff --git a/tests/TestUtility/OmniSharpTestHost.cs b/tests/TestUtility/OmniSharpTestHost.cs index 582a83091b..e2a17a8d89 100644 --- a/tests/TestUtility/OmniSharpTestHost.cs +++ b/tests/TestUtility/OmniSharpTestHost.cs @@ -13,6 +13,7 @@ using OmniSharp.DotNetTest.Models; using OmniSharp.Eventing; using OmniSharp.Mef; +using OmniSharp.Models.WorkspaceInformation; using OmniSharp.MSBuild; using OmniSharp.Roslyn.CSharp.Services; using OmniSharp.Services; @@ -32,7 +33,7 @@ public class OmniSharpTestHost : DisposableObject typeof(HostHelpers).GetTypeInfo().Assembly, // OmniSharp.Host typeof(DotNetProjectSystem).GetTypeInfo().Assembly, // OmniSharp.DotNet typeof(RunTestRequest).GetTypeInfo().Assembly, // OmniSharp.DotNetTest - typeof(MSBuildProjectSystem).GetTypeInfo().Assembly, // OmniSharp.MSBuild + typeof(ProjectSystem).GetTypeInfo().Assembly, // OmniSharp.MSBuild typeof(OmniSharpWorkspace).GetTypeInfo().Assembly, // OmniSharp.Roslyn typeof(RoslynFeaturesHostServicesProvider).GetTypeInfo().Assembly, // OmniSharp.Roslyn.CSharp typeof(CakeProjectSystem).GetTypeInfo().Assembly, // OmniSharp.Cake @@ -133,7 +134,13 @@ public static OmniSharpTestHost Create(string path = null, ITestOutputHelper tes WorkspaceInitializer.Initialize(serviceProvider, compositionHost, configuration, logger); - return new OmniSharpTestHost(serviceProvider, loggerFactory, workspace, compositionHost, oldMSBuildSdksPath); + var host = new OmniSharpTestHost(serviceProvider, loggerFactory, workspace, compositionHost, oldMSBuildSdksPath); + + // Force workspace to be updated + var service = host.GetWorkspaceInformationService(); + service.Handle(new WorkspaceInformationRequest()).Wait(); + + return host; } private static string SetMSBuildSdksPath(DotNetCliService dotNetCli)