diff --git a/.gitignore b/.gitignore index 330136f7e..ed84606bd 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,10 @@ bld/ # Visual Studio 2015 cache/options directory .vs/ + +# Visual Studio Code cache/options directory +.vscode/ + # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ diff --git a/Microsoft.DotNet.Cli.sln b/Microsoft.DotNet.Cli.sln index b9511a735..a0bfc5e8a 100644 --- a/Microsoft.DotNet.Cli.sln +++ b/Microsoft.DotNet.Cli.sln @@ -60,6 +60,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{0722D325-24C8-4E83-B5AF-0A083E7F0749}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MultiProjectValidator", "tools\MultiProjectValidator\MultiProjectValidator.xproj", "{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.ProjectModel.Server", "src\Microsoft.DotNet.ProjectModel.Server\Microsoft.DotNet.ProjectModel.Server.xproj", "{1EA9AF94-5494-40DD-A05B-9D564572CCFC}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.ProjectModel.Server.Tests", "test\Microsoft.DotNet.ProjectModel.Server.Tests\Microsoft.DotNet.ProjectModel.Server.Tests.xproj", "{11C77123-E4DA-499F-8900-80C88C2C69F2}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.DependencyModel", "src\Microsoft.Extensions.DependencyModel\Microsoft.Extensions.DependencyModel.xproj", "{688870C8-9843-4F9E-8576-D39290AD0F25}" EndProject @@ -477,6 +480,38 @@ Global {74F25188-BF63-4BF3-879B-B6CDB11ED608}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU {74F25188-BF63-4BF3-879B-B6CDB11ED608}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU {74F25188-BF63-4BF3-879B-B6CDB11ED608}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.Debug|x64.ActiveCfg = Debug|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.Debug|x64.Build.0 = Debug|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.MinSizeRel|x64.Build.0 = Debug|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.Release|Any CPU.Build.0 = Release|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.Release|x64.ActiveCfg = Release|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.Release|x64.Build.0 = Release|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {1EA9AF94-5494-40DD-A05B-9D564572CCFC}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.Debug|x64.Build.0 = Debug|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.MinSizeRel|x64.Build.0 = Debug|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.Release|Any CPU.Build.0 = Release|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.Release|x64.ActiveCfg = Release|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.Release|x64.Build.0 = Release|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {11C77123-E4DA-499F-8900-80C88C2C69F2}.RelWithDebInfo|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -507,5 +542,7 @@ Global {08A68C6A-86F6-4ED2-89A7-B166D33E9F85} = {0722D325-24C8-4E83-B5AF-0A083E7F0749} {688870C8-9843-4F9E-8576-D39290AD0F25} = {ED2FE3E2-F7E7-4389-8231-B65123F2076F} {74F25188-BF63-4BF3-879B-B6CDB11ED608} = {ED2FE3E2-F7E7-4389-8231-B65123F2076F} + {1EA9AF94-5494-40DD-A05B-9D564572CCFC} = {ED2FE3E2-F7E7-4389-8231-B65123F2076F} + {11C77123-E4DA-499F-8900-80C88C2C69F2} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7} EndGlobalSection EndGlobal diff --git a/scripts/compile.ps1 b/scripts/compile.ps1 index c932b8798..d5998b154 100644 --- a/scripts/compile.ps1 +++ b/scripts/compile.ps1 @@ -76,7 +76,15 @@ Download it from https://www.cmake.org # Restore packages header "Restoring packages" - & "$DnxRoot\dnu" restore "$RepoRoot" --quiet --runtime "$Rid" --no-cache + & "$DnxRoot\dnu" restore "$RepoRoot\src" --quiet --runtime "$Rid" --no-cache + & "$DnxRoot\dnu" restore "$RepoRoot\test" --quiet --runtime "$Rid" --no-cache + & "$DnxRoot\dnu" restore "$RepoRoot\tools" --quiet --runtime "$Rid" --no-cache + + $oldErrorAction=$ErrorActionPreference + $ErrorActionPreference="SilentlyContinue" + & "$DnxRoot\dnu" restore "$RepoRoot\testapp" --quiet --runtime "$Rid" --no-cache 2>&1 | Out-Null + $ErrorActionPreference=$oldErrorAction + if (!$?) { Write-Host "Command failed: " "$DnxRoot\dnu" restore "$RepoRoot" --quiet --runtime "$Rid" --no-cache Exit 1 diff --git a/scripts/compile.sh b/scripts/compile.sh index 8c412fa0f..7070eef3d 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -75,7 +75,12 @@ else PREFIX="$(cd -P "$(dirname "$DOTNET_PATH")/.." && pwd)" header "Restoring packages" - $DNX_ROOT/dnu restore "$REPOROOT" --quiet --runtime "$RID" --no-cache + $DNX_ROOT/dnu restore "$REPOROOT/src" --quiet --runtime "$RID" --no-cache + $DNX_ROOT/dnu restore "$REPOROOT/test" --quiet --runtime "$RID" --no-cache + $DNX_ROOT/dnu restore "$REPOROOT/tools" --quiet --runtime "$RID" --no-cache + set +e + $DNX_ROOT/dnu restore "$REPOROOT/testapp" --quiet --runtime "$RID" --no-cache >/dev/null 2>&1 + set -e fi header "Building corehost" diff --git a/src/Microsoft.DotNet.ProjectModel.Server/ConnectionContext.cs b/src/Microsoft.DotNet.ProjectModel.Server/ConnectionContext.cs new file mode 100644 index 000000000..a6c73bc45 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/ConnectionContext.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Net.Sockets; +using Microsoft.DotNet.ProjectModel.Server.Models; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ConnectionContext + { + private readonly string _hostName; + private readonly ProcessingQueue _queue; + private readonly IDictionary _projectContextManagers; + + public ConnectionContext(Socket acceptedSocket, + string hostName, + ProtocolManager protocolManager, + WorkspaceContext workspaceContext, + IDictionary projectContextManagers, + ILoggerFactory loggerFactory) + { + _hostName = hostName; + _projectContextManagers = projectContextManagers; + + _queue = new ProcessingQueue(new NetworkStream(acceptedSocket), loggerFactory); + _queue.OnReceive += message => + { + if (protocolManager.IsProtocolNegotiation(message)) + { + message.Sender = this; + protocolManager.Negotiate(message); + } + else + { + message.Sender = this; + ProjectContextManager keeper; + if (!_projectContextManagers.TryGetValue(message.ContextId, out keeper)) + { + keeper = new ProjectContextManager(message.ContextId, + loggerFactory, + workspaceContext, + protocolManager); + + _projectContextManagers[message.ContextId] = keeper; + } + + keeper.OnReceive(message); + } + }; + } + + public void QueueStart() + { + _queue.Start(); + } + + public bool Transmit(Message message) + { + message.HostId = _hostName; + return _queue.Send(message); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Helpers/DependencyTypeChangeFinder.cs b/src/Microsoft.DotNet.ProjectModel.Server/Helpers/DependencyTypeChangeFinder.cs new file mode 100644 index 000000000..ac13b6cc4 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Helpers/DependencyTypeChangeFinder.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Graph; + +namespace Microsoft.DotNet.ProjectModel.Server.Helpers +{ + internal class DependencyTypeChangeFinder + { + public static IEnumerable Diagnose( + ProjectContext context, + IEnumerable currentSearchPaths) + { + var result = new List(); + var project = context.ProjectFile; + var libraries = context.LibraryManager.GetLibraries(); + + var updatedSearchPath = GetUpdatedSearchPaths(currentSearchPaths, project.ResolveSearchPaths()); + var projectCandiates = GetProjectCandidates(updatedSearchPath); + var rootDependencies = libraries.FirstOrDefault(library => string.Equals(library.Identity.Name, project.Name)) + ?.Dependencies + ?.ToDictionary(libraryRange => libraryRange.Name); + + foreach (var library in libraries) + { + var diagnostic = Validate(library, projectCandiates, rootDependencies); + if (diagnostic != null) + { + result.Add(diagnostic); + } + } + + return result; + } + + private static DiagnosticMessage Validate(LibraryDescription library, + HashSet projectCandidates, + Dictionary rootDependencies) + { + if (!library.Resolved || projectCandidates == null) + { + return null; + } + + var foundCandidate = projectCandidates.Contains(library.Identity.Name); + + if ((library.Identity.Type == LibraryType.Project && !foundCandidate) || + (library.Identity.Type == LibraryType.Package && foundCandidate)) + { + library.Resolved = false; + + var libraryRange = rootDependencies[library.Identity.Name]; + + return new DiagnosticMessage( + ErrorCodes.NU1010, + $"The type of dependency {library.Identity.Name} was changed.", + libraryRange.SourceFilePath, + DiagnosticMessageSeverity.Error, + libraryRange.SourceLine, + libraryRange.SourceColumn, + library); + } + + return null; + } + + private static HashSet GetProjectCandidates(IEnumerable searchPaths) + { + if (searchPaths == null) + { + return null; + } + + return new HashSet(searchPaths.Where(path => Directory.Exists(path)) + .SelectMany(path => Directory.GetDirectories(path)) + .Where(path => File.Exists(Path.Combine(path, Project.FileName))) + .Select(path => Path.GetFileName(path))); + } + + /// + /// Returns the search paths if they're updated. Otherwise returns null. + /// + private static IEnumerable GetUpdatedSearchPaths(IEnumerable oldSearchPaths, + IEnumerable newSearchPaths) + { + // The oldSearchPaths is null when the current project is not initialized. It is not necessary to + // validate the dependency in this case. + if (oldSearchPaths == null) + { + return null; + } + + if (Enumerable.SequenceEqual(oldSearchPaths, newSearchPaths)) + { + return null; + } + + return newSearchPaths; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Helpers/JTokenExtensions.cs b/src/Microsoft.DotNet.ProjectModel.Server/Helpers/JTokenExtensions.cs new file mode 100644 index 000000000..9edd1d8bf --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Helpers/JTokenExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json.Linq; + +namespace Microsoft.DotNet.ProjectModel.Server.Helpers +{ + public static class JTokenExtensions + { + public static string GetValue(this JToken token, string name) + { + return GetValue(token, name); + } + + public static TVal GetValue(this JToken token, string name) + { + var value = token?[name]; + if (value != null) + { + return value.Value(); + } + + return default(TVal); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Helpers/NuGetFrameworkExtensions.cs b/src/Microsoft.DotNet.ProjectModel.Server/Helpers/NuGetFrameworkExtensions.cs new file mode 100644 index 000000000..27c2aba6f --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Helpers/NuGetFrameworkExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.ProjectModel.Resolution; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public static class NuGetFrameworkExtensions + { + public static FrameworkData ToPayload(this NuGetFramework framework, + FrameworkReferenceResolver resolver) + { + return new FrameworkData + { + ShortName = framework.GetShortFolderName(), + FrameworkName = framework.DotNetFrameworkName, + FriendlyName = framework.Framework, + RedistListPath = resolver.GetFrameworkRedistListPath(framework) + }; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Helpers/ProjectExtensions.cs b/src/Microsoft.DotNet.ProjectModel.Server/Helpers/ProjectExtensions.cs new file mode 100644 index 000000000..c7eb9b30e --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Helpers/ProjectExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.DotNet.ProjectModel.Server.Helpers +{ + public static class ProjectExtensions + { + public static IEnumerable ResolveSearchPaths(this Project project) + { + GlobalSettings settings; + return project.ResolveSearchPaths(out settings); + } + + public static IEnumerable ResolveSearchPaths(this Project project, out GlobalSettings globalSettings) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + var searchPaths = new HashSet { Directory.GetParent(project.ProjectDirectory).FullName }; + + globalSettings = project.ResolveGlobalSettings(); + if (globalSettings != null) + { + foreach (var searchPath in globalSettings.ProjectSearchPaths) + { + var path = Path.Combine(globalSettings.DirectoryPath, searchPath); + searchPaths.Add(Path.GetFullPath(path)); + } + } + + return searchPaths; + } + + public static GlobalSettings ResolveGlobalSettings(this Project project) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + GlobalSettings settings; + var root = ProjectRootResolver.ResolveRootDirectory(project.ProjectDirectory); + if (GlobalSettings.TryGetGlobalSettings(root, out settings)) + { + return settings; + } + else + { + return null; + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/DependencyInfo.cs b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/DependencyInfo.cs new file mode 100644 index 000000000..b0a21773f --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/DependencyInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.InternalModels +{ + internal class DependencyInfo + { + public Dictionary Dependencies { get; set; } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectInfo.cs b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectInfo.cs new file mode 100644 index 000000000..20af7171d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectInfo.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Server.Helpers; +using Microsoft.DotNet.ProjectModel.Server.Models; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Server.InternalModels +{ + internal class ProjectInfo + { + public ProjectInfo(ProjectContext context, + string configuration, + IEnumerable currentSearchPaths) + { + var allExports = context.CreateExporter(configuration).GetAllExports().ToList(); + var allDiagnostics = context.LibraryManager.GetAllDiagnostics(); + + Context = context; + Configuration = configuration; + + var allSourceFiles = new List(context.ProjectFile.Files.SourceFiles); + var allFileReferences = new List(); + + foreach (var export in allExports) + { + allSourceFiles.AddRange(export.SourceReferences); + allFileReferences.AddRange(export.CompilationAssemblies.Select(asset => asset.ResolvedPath)); + } + + SourceFiles = allSourceFiles.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(path => path).ToList(); + CompilationAssembiles = allFileReferences.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(path => path).ToList(); + + var allProjectReferences = new List(); + + var allDependencyDiagnostics = new List(); + allDependencyDiagnostics.AddRange(context.LibraryManager.GetAllDiagnostics()); + allDependencyDiagnostics.AddRange(DependencyTypeChangeFinder.Diagnose(Context, currentSearchPaths)); + + var diagnosticsLookup = allDependencyDiagnostics.ToLookup(d => d.Source); + + Dependencies = new Dictionary(); + + foreach (var library in context.LibraryManager.GetLibraries()) + { + var diagnostics = diagnosticsLookup[library].ToList(); + var description = DependencyDescription.Create(library, diagnostics); + Dependencies[description.Name] = description; + + if (library is ProjectDescription && library.Identity.Name != context.ProjectFile.Name) + { + allProjectReferences.Add(ProjectReferenceDescription.Create((ProjectDescription)library)); + } + } + + DependencyDiagnostics = allDependencyDiagnostics; + ProjectReferences = allProjectReferences.OrderBy(reference => reference.Name).ToList(); + } + + public string Configuration { get; } + + public ProjectContext Context { get; } + + public string RootDependency => Context.ProjectFile.Name; + + public NuGetFramework Framework => Context.TargetFramework; + + public CommonCompilerOptions CompilerOptions => Context.ProjectFile.GetCompilerOptions(Framework, Configuration); + + public IReadOnlyList SourceFiles { get; } + + public IReadOnlyList CompilationAssembiles { get; } + + public IReadOnlyList ProjectReferences { get; } + + public IReadOnlyList DependencyDiagnostics { get; } + + public Dictionary Dependencies { get; } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectSnapshot.cs b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectSnapshot.cs new file mode 100644 index 000000000..289cd76a5 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectSnapshot.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.DotNet.ProjectModel.Server.Models; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Server.InternalModels +{ + internal class ProjectSnapshot + { + public NuGetFramework TargetFramework { get; set; } + public IReadOnlyList SourceFiles { get; set; } + public CommonCompilerOptions CompilerOptions { get; set; } + public IReadOnlyList ProjectReferences { get; set; } + public IReadOnlyList FileReferences { get; set; } + public IReadOnlyList DependencyDiagnostics { get; set; } + public IDictionary Dependencies { get; set; } + public string RootDependency { get; set; } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectState.cs b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectState.cs new file mode 100644 index 000000000..fc7efe0e1 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/ProjectState.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using System.Collections.Generic; +using System; + +namespace Microsoft.DotNet.ProjectModel.Server.InternalModels +{ + internal class ProjectState + { + public static ProjectState Create(string appPath, + string configuration, + WorkspaceContext workspaceContext, + IEnumerable currentSearchPaths) + { + var projectContextsCollection = workspaceContext.GetProjectContextCollection(appPath); + if (!projectContextsCollection.ProjectContexts.Any()) + { + throw new InvalidOperationException($"Unable to find project.json in '{appPath}'"); + } + + var project = projectContextsCollection.ProjectContexts.First().ProjectFile; + var projectDiagnostics = new List(projectContextsCollection.ProjectDiagnostics); + var projectInfos = new List(); + + foreach (var projectContext in projectContextsCollection.ProjectContexts) + { + projectInfos.Add(new ProjectInfo( + projectContext, + configuration, + currentSearchPaths)); + } + + return new ProjectState(project, projectDiagnostics, projectInfos); + } + + private ProjectState(Project project, List projectDiagnostics, List projectInfos) + { + Project = project; + Projects = projectInfos; + Diagnostics = projectDiagnostics; + } + + public Project Project { get; } + + public IReadOnlyList Projects { get; } + + public IReadOnlyList Diagnostics { get; } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/Snapshot.cs b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/Snapshot.cs new file mode 100644 index 000000000..9ef3f6124 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/InternalModels/Snapshot.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Server.Helpers; +using Microsoft.DotNet.ProjectModel.Server.InternalModels; +using Microsoft.DotNet.ProjectModel.Server.Models; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class Snapshot + { + public static Snapshot CreateFromProject(Project project) + { + GlobalSettings globalSettings; + var projectSearchPaths = project.ResolveSearchPaths(out globalSettings); + + return new Snapshot(project, globalSettings?.FilePath, projectSearchPaths); + } + + public Snapshot() + { + Projects = new Dictionary(); + } + + public Snapshot(Project project, string globalJsonPath, IEnumerable projectSearchPaths) + : this() + { + Project = project; + GlobalJsonPath = globalJsonPath; + ProjectSearchPaths = projectSearchPaths.ToList(); + } + + public Project Project { get; set; } + public string GlobalJsonPath { get; set; } + public IReadOnlyList ProjectSearchPaths { get; set; } + public IReadOnlyList ProjectDiagnostics { get; set; } + public ErrorMessage GlobalErrorMessage { get; set; } + public Dictionary Projects { get; } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/MessageTypes.cs b/src/Microsoft.DotNet.ProjectModel.Server/MessageTypes.cs new file mode 100644 index 000000000..83aae8134 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/MessageTypes.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.Server +{ + public class MessageTypes + { + // Incoming + public const string ProjectContexts = nameof(ProjectContexts); + public const string Initialize = nameof(Initialize); + public const string ChangeConfiguration = nameof(ChangeConfiguration); + public const string RefreshDependencies = nameof(RefreshDependencies); + public const string RestoreComplete = nameof(RestoreComplete); + public const string FilesChanged = nameof(FilesChanged); + public const string GetDiagnostics = nameof(GetDiagnostics); + public const string ProtocolVersion = nameof(ProtocolVersion); + + // Outgoing + public const string Error = nameof(Error); + public const string ProjectInformation = nameof(ProjectInformation); + public const string Diagnostics = nameof(Diagnostics); + public const string DependencyDiagnostics = nameof(DependencyDiagnostics); + public const string Dependencies = nameof(Dependencies); + public const string CompilerOptions = nameof(CompilerOptions); + public const string References = nameof(References); + public const string Sources = nameof(Sources); + public const string AllDiagnostics = nameof(AllDiagnostics); + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/CompilerOptionsMessenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/CompilerOptionsMessenger.cs new file mode 100644 index 000000000..1450dbc6d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/CompilerOptionsMessenger.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.Server.InternalModels; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class CompilerOptionsMessenger : Messenger + { + public CompilerOptionsMessenger(Action transmit) + : base(MessageTypes.CompilerOptions, transmit) + { } + + protected override bool CheckDifference(ProjectSnapshot local, ProjectSnapshot remote) + { + return remote.CompilerOptions != null && + Equals(local.CompilerOptions, remote.CompilerOptions); + } + + protected override object CreatePayload(ProjectSnapshot local) + { + return new CompilationOptionsMessagePayload + { + Framework = local.TargetFramework.ToPayload(_resolver), + Options = local.CompilerOptions + }; + } + + protected override void SetValue(ProjectSnapshot local, ProjectSnapshot remote) + { + remote.CompilerOptions = local.CompilerOptions; + } + + private class CompilationOptionsMessagePayload + { + public FrameworkData Framework { get; set; } + + public CommonCompilerOptions Options { get; set; } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/DependenciesMessenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/DependenciesMessenger.cs new file mode 100644 index 000000000..728ba3eff --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/DependenciesMessenger.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Server.InternalModels; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class DependenciesMessenger : Messenger + { + public DependenciesMessenger(Action transmit) + : base(MessageTypes.Dependencies, transmit) + { } + + protected override bool CheckDifference(ProjectSnapshot local, ProjectSnapshot remote) + { + return remote.Dependencies != null && + string.Equals(local.RootDependency, remote.RootDependency) && + Equals(local.TargetFramework, remote.TargetFramework) && + Enumerable.SequenceEqual(local.Dependencies, remote.Dependencies); + } + + protected override object CreatePayload(ProjectSnapshot local) + { + return new DependenciesMessage + { + Framework = local.TargetFramework.ToPayload(_resolver), + RootDependency = local.RootDependency, + Dependencies = local.Dependencies + }; + } + + protected override void SetValue(ProjectSnapshot local, ProjectSnapshot remote) + { + remote.Dependencies = local.Dependencies; + } + + private class DependenciesMessage + { + public FrameworkData Framework { get; set; } + public string RootDependency { get; set; } + public IDictionary Dependencies { get; set; } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/DependencyDiagnosticsMessenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/DependencyDiagnosticsMessenger.cs new file mode 100644 index 000000000..abe3a6bc7 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/DependencyDiagnosticsMessenger.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Server.InternalModels; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class DependencyDiagnosticsMessenger : Messenger + { + public DependencyDiagnosticsMessenger(Action transmit) + : base(MessageTypes.DependencyDiagnostics, transmit) + { } + + protected override bool CheckDifference(ProjectSnapshot local, ProjectSnapshot remote) + { + return remote.DependencyDiagnostics != null && + Enumerable.SequenceEqual(local.DependencyDiagnostics, remote.DependencyDiagnostics); + } + + protected override object CreatePayload(ProjectSnapshot local) + { + return new DiagnosticsListMessage( + local.DependencyDiagnostics, + local.TargetFramework?.ToPayload(_resolver)); + } + + protected override void SetValue(ProjectSnapshot local, ProjectSnapshot remote) + { + remote.DependencyDiagnostics = local.DependencyDiagnostics; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/GlobalErrorMessenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/GlobalErrorMessenger.cs new file mode 100644 index 000000000..23b77255b --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/GlobalErrorMessenger.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class GlobalErrorMessenger : Messenger + { + public GlobalErrorMessenger(Action transmit) + : base(MessageTypes.Error, transmit) + { } + + protected override bool CheckDifference(Snapshot local, Snapshot remote) + { + return remote != null && Equals(local.GlobalErrorMessage, remote.GlobalErrorMessage); + } + + protected override object CreatePayload(Snapshot local) + { + return local.GlobalErrorMessage; + } + + protected override void SetValue(Snapshot local, Snapshot remote) + { + remote.GlobalErrorMessage = local.GlobalErrorMessage; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/Messenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/Messenger.cs new file mode 100644 index 000000000..7ec3d247b --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/Messenger.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.Resolution; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal abstract class Messenger where T : class + { + protected readonly FrameworkReferenceResolver _resolver; + protected readonly Action _transmit; + + public Messenger(string messageType, Action transmit) + { + _resolver = FrameworkReferenceResolver.Default; + _transmit = transmit; + + MessageType = messageType; + } + + public string MessageType { get; } + + public void UpdateRemote(T local, T remote) + { + if (!CheckDifference(local, remote)) + { + var payload = CreatePayload(local); + + _transmit(MessageType, payload); + + SetValue(local, remote); + } + } + + protected abstract void SetValue(T local, T remote); + protected abstract object CreatePayload(T local); + protected abstract bool CheckDifference(T local, T remote); + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ProjectDiagnosticsMessenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ProjectDiagnosticsMessenger.cs new file mode 100644 index 000000000..e93d74a87 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ProjectDiagnosticsMessenger.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class ProjectDiagnosticsMessenger : Messenger + { + public ProjectDiagnosticsMessenger(Action transmit) + : base(MessageTypes.Diagnostics, transmit) + { } + + protected override bool CheckDifference(Snapshot local, Snapshot remote) + { + return remote.ProjectDiagnostics != null && + Enumerable.SequenceEqual(local.ProjectDiagnostics, remote.ProjectDiagnostics); + } + + protected override object CreatePayload(Snapshot local) + { + return new DiagnosticsListMessage(local.ProjectDiagnostics); + } + + protected override void SetValue(Snapshot local, Snapshot remote) + { + remote.ProjectDiagnostics = local.ProjectDiagnostics; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ProjectInformationMessenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ProjectInformationMessenger.cs new file mode 100644 index 000000000..715498046 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ProjectInformationMessenger.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Resolution; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class ProjectInformationMessenger : Messenger + { + public ProjectInformationMessenger(Action transmit) + : base(MessageTypes.ProjectInformation, transmit) + { } + + protected override bool CheckDifference(Snapshot local, Snapshot remote) + { + return remote.Project != null && + string.Equals(local.Project.Name, remote.Project.Name) && + string.Equals(local.GlobalJsonPath, remote.GlobalJsonPath) && + Enumerable.SequenceEqual(local.Project.GetTargetFrameworks().Select(f => f.FrameworkName), + remote.Project.GetTargetFrameworks().Select(f => f.FrameworkName)) && + Enumerable.SequenceEqual(local.Project.GetConfigurations(), remote.Project.GetConfigurations()) && + Enumerable.SequenceEqual(local.Project.Commands, remote.Project.Commands) && + Enumerable.SequenceEqual(local.ProjectSearchPaths, remote.ProjectSearchPaths); + } + + protected override object CreatePayload(Snapshot local) + { + return new ProjectInformation(local.Project, local.GlobalJsonPath, local.ProjectSearchPaths, _resolver); + } + + protected override void SetValue(Snapshot local, Snapshot remote) + { + remote.Project = local.Project; + remote.GlobalJsonPath = local.GlobalJsonPath; + remote.ProjectSearchPaths = local.ProjectSearchPaths; + } + + private class ProjectInformation + { + public ProjectInformation(Project project, + string gloablJsonPath, + IEnumerable projectSearchPath, + FrameworkReferenceResolver resolver) + { + Name = project.Name; + Frameworks = project.GetTargetFrameworks().Select(f => f.FrameworkName.ToPayload(resolver)).ToList(); + Configurations = project.GetConfigurations().ToList(); + Commands = project.Commands; + ProjectSearchPaths = new List(projectSearchPath); + GlobalJsonPath = gloablJsonPath; + } + + public string Name { get; } + + public IReadOnlyList Frameworks { get; } + + public IReadOnlyList Configurations { get; } + + public IDictionary Commands { get; } + + public IReadOnlyList ProjectSearchPaths { get; } + + public string GlobalJsonPath { get; } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ReferencesMessenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ReferencesMessenger.cs new file mode 100644 index 000000000..50127875c --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/ReferencesMessenger.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Server.InternalModels; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class ReferencesMessenger : Messenger + { + public ReferencesMessenger(Action transmit) + : base(MessageTypes.References, transmit) + { } + + protected override bool CheckDifference(ProjectSnapshot local, ProjectSnapshot remote) + { + return remote.FileReferences != null && + remote.ProjectReferences != null && + Enumerable.SequenceEqual(local.FileReferences, remote.FileReferences) && + Enumerable.SequenceEqual(local.ProjectReferences, remote.ProjectReferences); + } + + protected override object CreatePayload(ProjectSnapshot local) + { + return new ReferencesMessage + { + Framework = local.TargetFramework.ToPayload(_resolver), + ProjectReferences = local.ProjectReferences, + FileReferences = local.FileReferences + }; + } + + protected override void SetValue(ProjectSnapshot local, ProjectSnapshot remote) + { + remote.FileReferences = local.FileReferences; + remote.ProjectReferences = local.ProjectReferences; + } + + private class ReferencesMessage + { + public FrameworkData Framework { get; set; } + public IReadOnlyList FileReferences { get; set; } + public IReadOnlyList ProjectReferences { get; set; } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Messengers/SourcesMessenger.cs b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/SourcesMessenger.cs new file mode 100644 index 000000000..77e0473bf --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Messengers/SourcesMessenger.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Server.InternalModels; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class SourcesMessenger : Messenger + { + public SourcesMessenger(Action transmit) + : base(MessageTypes.Sources, transmit) + { } + + protected override bool CheckDifference(ProjectSnapshot local, ProjectSnapshot remote) + { + return remote.SourceFiles != null && + Enumerable.SequenceEqual(local.SourceFiles, remote.SourceFiles); + } + + protected override object CreatePayload(ProjectSnapshot local) + { + return new SourcesMessagePayload + { + Framework = local.TargetFramework.ToPayload(_resolver), + Files = local.SourceFiles, + GeneratedFiles = new Dictionary() + }; + } + + protected override void SetValue(ProjectSnapshot local, ProjectSnapshot remote) + { + remote.SourceFiles = local.SourceFiles; + } + + private class SourcesMessagePayload + { + public FrameworkData Framework { get; set; } + public IReadOnlyList Files { get; set; } + public IDictionary GeneratedFiles { get; set; } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Microsoft.DotNet.ProjectModel.Server.xproj b/src/Microsoft.DotNet.ProjectModel.Server/Microsoft.DotNet.ProjectModel.Server.xproj new file mode 100644 index 000000000..4111ad8d2 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Microsoft.DotNet.ProjectModel.Server.xproj @@ -0,0 +1,19 @@ + + + + 14.0.23107 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 1ea9af94-5494-40dd-a05b-9d564572ccfc + Microsoft.DotNet.ProjectModel.Server + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/DependencyDescription.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/DependencyDescription.cs new file mode 100644 index 000000000..35948eca0 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/DependencyDescription.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Graph; + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public class DependencyDescription + { + private DependencyDescription() { } + + public static DependencyDescription Create(LibraryDescription library, IEnumerable diagnostics) + { + return new DependencyDescription + { + Name = library.Identity.Name, + DisplayName = GetLibraryDisplayName(library), + Version = library.Identity.Version?.ToString(), + Type = library.Identity.Type.Value, + Resolved = library.Resolved, + Path = library.Path, + Dependencies = library.Dependencies.Select(dependency => new DependencyItem + { + Name = dependency.Name, + Version = dependency.VersionRange?.ToString() // TODO: review + }), + Errors = diagnostics.Where(d => d.Severity == DiagnosticMessageSeverity.Error) + .Select(d => new DiagnosticMessageView(d)), + Warnings = diagnostics.Where(d => d.Severity == DiagnosticMessageSeverity.Warning) + .Select(d => new DiagnosticMessageView(d)) + }; + } + + public string Name { get; private set; } + + public string DisplayName { get; private set; } + + public string Version { get; private set; } + + public string Path { get; private set; } + + public string Type { get; private set; } + + public bool Resolved { get; private set; } + + public IEnumerable Dependencies { get; private set; } + + public IEnumerable Errors { get; private set; } + + public IEnumerable Warnings { get; private set; } + + public override bool Equals(object obj) + { + var other = obj as DependencyDescription; + + return other != null && + Resolved == other.Resolved && + string.Equals(Name, other.Name) && + object.Equals(Version, other.Version) && + string.Equals(Path, other.Path) && + string.Equals(Type, other.Type) && + Enumerable.SequenceEqual(Dependencies, other.Dependencies) && + Enumerable.SequenceEqual(Errors, other.Errors) && + Enumerable.SequenceEqual(Warnings, other.Warnings); + } + + public override int GetHashCode() + { + // These objects are currently POCOs and we're overriding equals + // so that things like Enumerable.SequenceEqual just work. + return base.GetHashCode(); + } + + private static string GetLibraryDisplayName(LibraryDescription library) + { + var name = library.Identity.Name; + if (library.Identity.Type == LibraryType.ReferenceAssembly && name.StartsWith("fx/")) + { + name = name.Substring(3); + } + + return name; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/DependencyItem.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/DependencyItem.cs new file mode 100644 index 000000000..1f014daff --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/DependencyItem.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public class DependencyItem + { + public string Name { get; set; } + + public string Version { get; set; } + + public override bool Equals(object obj) + { + var other = obj as DependencyItem; + return other != null && + string.Equals(Name, other.Name) && + object.Equals(Version, other.Version); + } + + public override int GetHashCode() + { + // These objects are currently POCOs and we're overriding equals + // so that things like Enumerable.SequenceEqual just work. + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticMessageGroup.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticMessageGroup.cs new file mode 100644 index 000000000..c76adf86c --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticMessageGroup.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public class DiagnosticMessageGroup + { + public DiagnosticMessageGroup(IEnumerable diagnostics) + : this(framework: null, diagnostics: diagnostics) + { } + + public DiagnosticMessageGroup(NuGetFramework framework, IEnumerable diagnostics) + { + Framework = framework; + Diagnostics = diagnostics; + } + + public IEnumerable Diagnostics { get; } + + public NuGetFramework Framework { get; } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticMessageView.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticMessageView.cs new file mode 100644 index 000000000..8285d48ba --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticMessageView.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public class DiagnosticMessageView + { + public DiagnosticMessageView(DiagnosticMessage data) + { + ErrorCode = data.ErrorCode; + SourceFilePath = data.SourceFilePath; + Message = data.Message; + Severity = data.Severity; + StartLine = data.StartLine; + StartColumn = data.StartColumn; + EndLine = data.EndLine; + EndColumn = data.EndColumn; + FormattedMessage = data.FormattedMessage; + + var description = data.Source as LibraryDescription; + if (description != null) + { + Source = new + { + Name = description.Identity.Name, + Version = description.Identity.Version?.ToString() + }; + } + } + + public string ErrorCode { get; } + + public string SourceFilePath { get; } + + public string Message { get; } + + public DiagnosticMessageSeverity Severity { get; } + + public int StartLine { get; } + + public int StartColumn { get; } + + public int EndLine { get; } + + public int EndColumn { get; } + + public string FormattedMessage { get; } + + public object Source { get; } + + public override bool Equals(object obj) + { + var other = obj as DiagnosticMessageView; + + return other != null && + Severity == other.Severity && + StartLine == other.StartLine && + StartColumn == other.StartColumn && + EndLine == other.EndLine && + EndColumn == other.EndColumn && + string.Equals(ErrorCode, other.ErrorCode, StringComparison.Ordinal) && + string.Equals(SourceFilePath, other.SourceFilePath, StringComparison.Ordinal) && + string.Equals(Message, other.Message, StringComparison.Ordinal) && + object.Equals(Source, other.Source); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticsListMessage.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticsListMessage.cs new file mode 100644 index 000000000..e8ae452dd --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/DiagnosticsListMessage.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public class DiagnosticsListMessage + { + public DiagnosticsListMessage(IEnumerable diagnostics) : + this(diagnostics, frameworkData: null) + { + } + + public DiagnosticsListMessage(IEnumerable diagnostics, FrameworkData frameworkData) : + this(diagnostics.Select(msg => new DiagnosticMessageView(msg)).ToList(), frameworkData) + { + if (diagnostics == null) + { + throw new ArgumentNullException(nameof(diagnostics)); + } + } + + public DiagnosticsListMessage(IEnumerable diagnostics) : + this(diagnostics, frameworkData: null) + { + } + + public DiagnosticsListMessage(IEnumerable diagnostics, FrameworkData frameworkData) + { + if (diagnostics == null) + { + throw new ArgumentNullException(nameof(diagnostics)); + } + + Diagnostics = diagnostics; + Errors = diagnostics.Where(msg => msg.Severity == DiagnosticMessageSeverity.Error).ToList(); + Warnings = diagnostics.Where(msg => msg.Severity == DiagnosticMessageSeverity.Warning).ToList(); + Framework = frameworkData; + } + + public FrameworkData Framework { get; } + + [JsonIgnore] + public IEnumerable Diagnostics { get; } + + public IList Errors { get; } + + public IList Warnings { get; } + + public override bool Equals(object obj) + { + var other = obj as DiagnosticsListMessage; + + return other != null && + Enumerable.SequenceEqual(Errors, other.Errors) && + Enumerable.SequenceEqual(Warnings, other.Warnings) && + object.Equals(Framework, other.Framework); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/ErrorMessage.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/ErrorMessage.cs new file mode 100644 index 000000000..8623afae3 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/ErrorMessage.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public class ErrorMessage + { + public string Message { get; set; } + public string Path { get; set; } + public int Line { get; set; } + public int Column { get; set; } + + public override bool Equals(object obj) + { + var payload = obj as ErrorMessage; + return payload != null && + string.Equals(Message, payload.Message, StringComparison.Ordinal) && + string.Equals(Path, payload.Path, StringComparison.OrdinalIgnoreCase) && + Line == payload.Line && + Column == payload.Column; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/FrameworkData.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/FrameworkData.cs new file mode 100644 index 000000000..258c1cb08 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/FrameworkData.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public class FrameworkData + { + public string FrameworkName { get; set; } + public string FriendlyName { get; set; } + public string ShortName { get; set; } + public string RedistListPath { get; set; } + + public override bool Equals(object obj) + { + var other = obj as FrameworkData; + + return other != null && + string.Equals(FrameworkName, other.FrameworkName); + } + + public override int GetHashCode() + { + // These objects are currently POCOs and we're overriding equals + // so that things like Enumerable.SequenceEqual just work. + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/Message.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/Message.cs new file mode 100644 index 000000000..0c0bfe8f3 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/Message.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + internal class Message + { + public static Message FromPayload(string messageType, int contextId, object payload) + { + return new Message + { + MessageType = messageType, + ContextId = contextId, + Payload = payload is JToken ? (JToken)payload : JToken.FromObject(payload) + }; + } + + private Message() { } + + public string MessageType { get; set; } + + public string HostId { get; set; } + + public int ContextId { get; set; } = -1; + + public JToken Payload { get; set; } + + [JsonIgnore] + public ConnectionContext Sender { get; set; } + + public override string ToString() + { + return $"({HostId}, {MessageType}, {ContextId}) -> {Payload?.ToString(Formatting.Indented)}"; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Models/ProjectReferenceDescription.cs b/src/Microsoft.DotNet.ProjectModel.Server/Models/ProjectReferenceDescription.cs new file mode 100644 index 000000000..019c2062a --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Models/ProjectReferenceDescription.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + internal class ProjectReferenceDescription + { + private ProjectReferenceDescription() { } + + public static ProjectReferenceDescription Create(ProjectDescription description) + { + var targetFrameworkInformation = description.TargetFrameworkInfo; + + string wrappedProjectPath = null; + if (!string.IsNullOrEmpty(targetFrameworkInformation?.WrappedProject) && + description.Project != null) + { + wrappedProjectPath = System.IO.Path.Combine( + description.Project.ProjectDirectory, + targetFrameworkInformation.WrappedProject); + + wrappedProjectPath = System.IO.Path.GetFullPath(wrappedProjectPath); + } + + return new ProjectReferenceDescription + { + Name = description.Identity.Name, + Path = description.Path, + WrappedProjectPath = wrappedProjectPath, + }; + } + + public string Name { get; set; } + public string Path { get; set; } + public string WrappedProjectPath { get; set; } + + public override bool Equals(object obj) + { + var other = obj as ProjectReferenceDescription; + return other != null && + string.Equals(Name, other.Name) && + string.Equals(Path, other.Path) && + string.Equals(WrappedProjectPath, other.WrappedProjectPath); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/ProcessingQueue.cs b/src/Microsoft.DotNet.ProjectModel.Server/ProcessingQueue.cs new file mode 100644 index 000000000..c9e2619f3 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/ProcessingQueue.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Threading; +using Microsoft.DotNet.ProjectModel.Server.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ProcessingQueue + { + private readonly BinaryReader _reader; + private readonly BinaryWriter _writer; + private readonly ILogger _log; + + public ProcessingQueue(Stream stream, ILoggerFactory loggerFactory) + { + _reader = new BinaryReader(stream); + _writer = new BinaryWriter(stream); + _log = loggerFactory.CreateLogger(); + } + + public event Action OnReceive; + + public void Start() + { + _log.LogInformation("Start"); + new Thread(ReceiveMessages).Start(); + } + + public bool Send(Action writeAction) + { + lock (_writer) + { + try + { + writeAction(_writer); + return true; + } + catch (IOException ex) + { + // swallow + _log.LogWarning($"Ignore {nameof(IOException)} during sending message: \"{ex.Message}\"."); + } + catch (Exception ex) + { + _log.LogWarning($"Unexpected exception {ex.GetType().Name} during sending message: \"{ex.Message}\"."); + throw; + } + } + + return false; + } + + public bool Send(Message message) + { + return Send(_writer => + { + _log.LogInformation($"Send ({message})"); + _writer.Write(JsonConvert.SerializeObject(message)); + }); + } + + private void ReceiveMessages() + { + try + { + while (true) + { + var content = _reader.ReadString(); + var message = JsonConvert.DeserializeObject(content); + + _log.LogInformation($"OnReceive({message})"); + OnReceive(message); + } + } + catch (IOException ex) + { + _log.LogWarning($"Ignore {nameof(IOException)} during receiving messages: \"{ex}\"."); + } + catch (Exception ex) + { + _log.LogError($"Unexpected exception {ex.GetType().Name} during receiving messages: \"{ex}\"."); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel.Server/Program.cs b/src/Microsoft.DotNet.ProjectModel.Server/Program.cs new file mode 100644 index 000000000..293bae263 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/Program.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using Microsoft.Dnx.Runtime.Common.CommandLine; +using Microsoft.DotNet.ProjectModel.Resolution; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + public class Program + { + private readonly Dictionary _projectContextManagers; + private readonly WorkspaceContext _workspaceContext; + private readonly ProtocolManager _protocolManager; + private readonly ILoggerFactory _loggerFactory; + private readonly string _hostName; + private readonly int _port; + private Socket _listenSocket; + + public Program(int intPort, string hostName, ILoggerFactory loggerFactory) + { + _port = intPort; + _hostName = hostName; + _loggerFactory = loggerFactory; + _protocolManager = new ProtocolManager(maxVersion: 4, loggerFactory: _loggerFactory); + _workspaceContext = WorkspaceContext.Create(); + _projectContextManagers = new Dictionary(); + } + + public static int Main(string[] args) + { + var app = new CommandLineApplication(); + app.Name = "dotnet-projectmodel-server"; + app.Description = ".NET Project Model Server"; + app.FullName = ".NET Design Time Server"; + app.Description = ".NET Design Time Server"; + app.HelpOption("-?|-h|--help"); + + var verbose = app.Option("--verbose", "Verbose ouput", CommandOptionType.NoValue); + var hostpid = app.Option("--hostPid", "The process id of the host", CommandOptionType.SingleValue); + var hostname = app.Option("--hostName", "The name of the host", CommandOptionType.SingleValue); + var port = app.Option("--port", "The TCP port used for communication", CommandOptionType.SingleValue); + + app.OnExecute(() => + { + var loggerFactory = new LoggerFactory(); + loggerFactory.AddConsole(verbose.HasValue() ? LogLevel.Debug : LogLevel.Information); + + var logger = loggerFactory.CreateLogger(); + + if (!MonitorHostProcess(hostpid, logger)) + { + return 1; + } + + var intPort = CheckPort(port, logger); + if (intPort == -1) + { + return 1; + } + + if (!hostname.HasValue()) + { + logger.LogError($"Option \"{hostname.LongName}\" is missing."); + return 1; + } + + var program = new Program(intPort, hostname.Value(), loggerFactory); + program.OpenChannel(); + + return 0; + }); + + return app.Execute(args); + } + + public void OpenChannel() + { + var logger = _loggerFactory.CreateLogger($"OpenChannel"); + + // This fixes the mono incompatibility but ties it to ipv4 connections + _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _listenSocket.Bind(new IPEndPoint(IPAddress.Loopback, _port)); + _listenSocket.Listen(10); + + logger.LogInformation($"Process ID {Process.GetCurrentProcess().Id}"); + logger.LogInformation($"Listening on port {_port}"); + + while (true) + { + var acceptSocket = _listenSocket.Accept(); + logger.LogInformation($"Client accepted {acceptSocket.LocalEndPoint}"); + + var connection = new ConnectionContext(acceptSocket, + _hostName, + _protocolManager, + _workspaceContext, + _projectContextManagers, + _loggerFactory); + + connection.QueueStart(); + } + } + + public void Shutdown() + { + if (_listenSocket.Connected) + { + _listenSocket.Shutdown(SocketShutdown.Both); + } + } + + private static int CheckPort(CommandOption port, ILogger logger) + { + if (!port.HasValue()) + { + logger.LogError($"Option \"{port.LongName}\" is missing."); + } + + int result; + if (int.TryParse(port.Value(), out result)) + { + return result; + } + else + { + logger.LogError($"Option \"{port.LongName}\" is not a valid Int32 value."); + return -1; + } + } + + private static bool MonitorHostProcess(CommandOption host, ILogger logger) + { + if (!host.HasValue()) + { + logger.LogError($"Option \"{host.LongName}\" is missing."); + return false; + } + + int hostPID; + if (int.TryParse(host.Value(), out hostPID)) + { + var hostProcess = Process.GetProcessById(hostPID); + hostProcess.EnableRaisingEvents = true; + hostProcess.Exited += (s, e) => + { + Process.GetCurrentProcess().Kill(); + }; + + logger.LogDebug($"Server will exit when process {hostPID} exits."); + return true; + } + else + { + logger.LogError($"Option \"{host.LongName}\" is not a valid Int32 value."); + return false; + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/ProjectContextManager.cs b/src/Microsoft.DotNet.ProjectModel.Server/ProjectContextManager.cs new file mode 100644 index 000000000..52b27a255 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/ProjectContextManager.cs @@ -0,0 +1,401 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using Microsoft.DotNet.ProjectModel.Resolution; +using Microsoft.DotNet.ProjectModel.Server.Helpers; +using Microsoft.DotNet.ProjectModel.Server.InternalModels; +using Microsoft.DotNet.ProjectModel.Server.Messengers; +using Microsoft.DotNet.ProjectModel.Server.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ProjectContextManager + { + private readonly ILogger _log; + + private readonly object _processingLock = new object(); + private readonly Queue _inbox = new Queue(); + private readonly ProtocolManager _protocolManager; + private readonly List _waitingForDiagnostics = new List(); + + private ConnectionContext _initializedContext; + + // triggers + private readonly Trigger _appPath = new Trigger(); + private readonly Trigger _configure = new Trigger(); + private readonly Trigger _refreshDependencies = new Trigger(); + private readonly Trigger _filesChanged = new Trigger(); + + private Snapshot _local = new Snapshot(); + private Snapshot _remote = new Snapshot(); + + private readonly WorkspaceContext _workspaceContext; + private int? _contextProtocolVersion; + + private readonly List> _messengers; + + private ProjectDiagnosticsMessenger _projectDiagnosticsMessenger; + private GlobalErrorMessenger _globalErrorMessenger; + private ProjectInformationMessenger _projectInforamtionMessenger; + + public ProjectContextManager(int contextId, + ILoggerFactory loggerFactory, + WorkspaceContext workspaceContext, + ProtocolManager protocolManager) + { + Id = contextId; + _log = loggerFactory.CreateLogger(); + _workspaceContext = workspaceContext; + _protocolManager = protocolManager; + + _messengers = new List> + { + new DependencyDiagnosticsMessenger(Transmit), + new DependenciesMessenger(Transmit), + new CompilerOptionsMessenger(Transmit), + new ReferencesMessenger(Transmit), + new SourcesMessenger(Transmit) + }; + + _projectDiagnosticsMessenger = new ProjectDiagnosticsMessenger(Transmit); + _globalErrorMessenger = new GlobalErrorMessenger(TransmitDiagnostics); + _projectInforamtionMessenger = new ProjectInformationMessenger(Transmit); + } + + public int Id { get; } + + public string ProjectPath { get { return _appPath.Value; } } + + public int ProtocolVersion + { + get + { + if (_contextProtocolVersion.HasValue) + { + return _contextProtocolVersion.Value; + } + else + { + return _protocolManager.CurrentVersion; + } + } + } + + public void OnReceive(Message message) + { + lock (_inbox) + { + _inbox.Enqueue(message); + } + + ThreadPool.QueueUserWorkItem(state => ((ProjectContextManager)state).ProcessLoop(), this); + } + + private void Transmit(string messageType, object payload) + { + var message = Message.FromPayload(messageType, Id, payload); + _initializedContext.Transmit(message); + } + + private void TransmitDiagnostics(string messageType, object payload) + { + var message = Message.FromPayload(messageType, Id, payload); + _initializedContext.Transmit(message); + + foreach (var connection in _waitingForDiagnostics) + { + connection.Transmit(message); + } + } + + private void ProcessLoop() + { + if (!Monitor.TryEnter(_processingLock)) + { + return; + } + + try + { + lock (_inbox) + { + if (!_inbox.Any()) + { + return; + } + } + + DoProcessLoop(); + } + catch (Exception ex) + { + // TODO: review error handing logic + + _log.LogError($"Error occurred: {ex}"); + + var error = new ErrorMessage + { + Message = ex.Message + }; + + var fileFormatException = ex as FileFormatException; + if (fileFormatException != null) + { + error.Path = fileFormatException.Path; + error.Line = fileFormatException.Line; + error.Column = fileFormatException.Column; + } + + var message = Message.FromPayload(MessageTypes.Error, Id, error); + + _initializedContext.Transmit(message); + _remote.GlobalErrorMessage = error; + + foreach (var connection in _waitingForDiagnostics) + { + connection.Transmit(message); + } + + _waitingForDiagnostics.Clear(); + } + } + + private void DoProcessLoop() + { + while (true) + { + DrainInbox(); + + var allDiagnostics = new List(); + + UpdateProjectStates(); + SendOutgingMessages(allDiagnostics); + SendDiagnostics(allDiagnostics); + + lock (_inbox) + { + if (_inbox.Count == 0) + { + return; + } + } + } + } + + private void DrainInbox() + { + _log.LogInformation("Begin draining inbox."); + + while (ProcessMessage()) { } + + _log.LogInformation("Finish draining inbox."); + } + + private bool ProcessMessage() + { + Message message; + + lock (_inbox) + { + if (!_inbox.Any()) + { + return false; + } + + message = _inbox.Dequeue(); + Debug.Assert(message != null); + } + + _log.LogInformation($"Received {message.MessageType}"); + + switch (message.MessageType) + { + case MessageTypes.Initialize: + Initialize(message); + break; + case MessageTypes.ChangeConfiguration: + // TODO: what if the payload is null or represent empty string? + _configure.Value = message.Payload.GetValue("Configuration"); + break; + case MessageTypes.RefreshDependencies: + case MessageTypes.RestoreComplete: + _refreshDependencies.Value = 0; + break; + case MessageTypes.FilesChanged: + _filesChanged.Value = 0; + break; + case MessageTypes.GetDiagnostics: + _waitingForDiagnostics.Add(message.Sender); + break; + } + + return true; + } + + private void Initialize(Message message) + { + if (_initializedContext != null) + { + _log.LogWarning($"Received {message.MessageType} message more than once for {_appPath.Value}"); + return; + } + + _initializedContext = message.Sender; + _appPath.Value = message.Payload.GetValue("ProjectFolder"); + _configure.Value = message.Payload.GetValue("Configuration") ?? "Debug"; + + var version = message.Payload.GetValue("Version"); + if (version != 0 && !_protocolManager.EnvironmentOverridden) + { + _contextProtocolVersion = Math.Min(version, _protocolManager.MaxVersion); + _log.LogInformation($"Set context protocol version to {_contextProtocolVersion.Value}"); + } + } + + private bool UpdateProjectStates() + { + ProjectState state = null; + + if (_appPath.WasAssigned || _configure.WasAssigned || _filesChanged.WasAssigned || _refreshDependencies.WasAssigned) + { + _appPath.ClearAssigned(); + _configure.ClearAssigned(); + _filesChanged.ClearAssigned(); + _refreshDependencies.ClearAssigned(); + + state = ProjectState.Create(_appPath.Value, _configure.Value, _workspaceContext, _remote.ProjectSearchPaths); + } + + if (state == null) + { + return false; + } + + var frameworkReferenceResolver = FrameworkReferenceResolver.Default; + + _local = Snapshot.CreateFromProject(state.Project); + _local.ProjectDiagnostics = state.Diagnostics; + + foreach (var projectInfo in state.Projects) + { + var projectWorkd = new ProjectSnapshot + { + RootDependency = projectInfo.RootDependency, + TargetFramework = projectInfo.Framework, + SourceFiles = new List(projectInfo.SourceFiles), + CompilerOptions = projectInfo.CompilerOptions, + ProjectReferences = projectInfo.ProjectReferences, + FileReferences = projectInfo.CompilationAssembiles, + DependencyDiagnostics = projectInfo.DependencyDiagnostics, + Dependencies = projectInfo.Dependencies + }; + + _local.Projects[projectInfo.Framework] = projectWorkd; + } + + return true; + } + + private void SendOutgingMessages(List diagnostics) + { + _projectInforamtionMessenger.UpdateRemote(_local, _remote); + _projectDiagnosticsMessenger.UpdateRemote(_local, _remote); + + if (_local.ProjectDiagnostics != null) + { + diagnostics.Add(new DiagnosticMessageGroup(_local.ProjectDiagnostics)); + } + + var unprocessedFrameworks = new HashSet(_remote.Projects.Keys); + foreach (var pair in _local.Projects) + { + ProjectSnapshot localProjectSnapshot = pair.Value; + ProjectSnapshot remoteProjectSnapshot; + + if (!_remote.Projects.TryGetValue(pair.Key, out remoteProjectSnapshot)) + { + remoteProjectSnapshot = new ProjectSnapshot(); + _remote.Projects[pair.Key] = remoteProjectSnapshot; + } + + if (localProjectSnapshot.DependencyDiagnostics != null) + { + diagnostics.Add(new DiagnosticMessageGroup( + localProjectSnapshot.TargetFramework, + localProjectSnapshot.DependencyDiagnostics)); + } + + unprocessedFrameworks.Remove(pair.Key); + + foreach(var messenger in _messengers) + { + messenger.UpdateRemote(localProjectSnapshot, + remoteProjectSnapshot); + } + } + + // Remove all processed frameworks from the remote view + foreach (var framework in unprocessedFrameworks) + { + _remote.Projects.Remove(framework); + } + } + + private void SendDiagnostics(List allDiagnostics) + { + _log.LogInformation($"SendDiagnostics, {allDiagnostics.Count()} diagnostics, {_waitingForDiagnostics.Count()} waiting for diagnostics."); + if (!allDiagnostics.Any()) + { + return; + } + + _globalErrorMessenger.UpdateRemote(_local, _remote); + + // Group all of the diagnostics into group by target framework + var messages = new List(); + foreach (var g in allDiagnostics.GroupBy(g => g.Framework)) + { + var frameworkData = g.Key?.ToPayload(FrameworkReferenceResolver.Default); + var messageGroup = g.SelectMany(d => d.Diagnostics).ToList(); + messages.Add(new DiagnosticsListMessage(messageGroup, frameworkData)); + } + + // Send all diagnostics back + TransmitDiagnostics( + MessageTypes.AllDiagnostics, + messages.Select(d => JToken.FromObject(d))); + + _waitingForDiagnostics.Clear(); + } + + private class Trigger + { + private TValue _value; + + public bool WasAssigned { get; private set; } + + public void ClearAssigned() + { + WasAssigned = false; + } + + public TValue Value + { + get { return _value; } + set + { + WasAssigned = true; + _value = value; + } + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/ProtocolManager.cs b/src/Microsoft.DotNet.ProjectModel.Server/ProtocolManager.cs new file mode 100644 index 000000000..bc3188c86 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/ProtocolManager.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.Server.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ProtocolManager + { + /// + /// Environment variable for overriding protocol. + /// + public const string EnvDthProtocol = "DTH_PROTOCOL"; + + private readonly ILogger _log; + + public ProtocolManager(int maxVersion, ILoggerFactory loggerFactory) + { + MaxVersion = maxVersion; + _log = loggerFactory.CreateLogger(); + + // initialized to the highest supported version or environment overridden value + int? protocol = GetProtocolVersionFromEnvironment(); + + if (protocol.HasValue) + { + CurrentVersion = protocol.Value; + EnvironmentOverridden = true; + } + else + { + CurrentVersion = 4; + } + } + + public int MaxVersion { get; } + + public int CurrentVersion { get; private set; } + + public bool EnvironmentOverridden { get; } + + public bool IsProtocolNegotiation(Message message) + { + return message?.MessageType == MessageTypes.ProtocolVersion; + } + + public void Negotiate(Message message) + { + if (!IsProtocolNegotiation(message)) + { + return; + } + + _log.LogInformation("Initializing the protocol negotiation."); + + if (EnvironmentOverridden) + { + _log.LogInformation($"DTH protocol negotiation is override by environment variable {EnvDthProtocol} and set to {CurrentVersion}."); + return; + } + + var tokenValue = message.Payload?["Version"]; + if (tokenValue == null) + { + _log.LogInformation("Protocol negotiation failed. Version property is missing in payload."); + return; + } + + var preferredVersion = tokenValue.ToObject(); + if (preferredVersion == 0) + { + // the preferred version can't be zero. either property is missing or the the payload is corrupted. + _log.LogInformation("Protocol negotiation failed. Protocol version 0 is invalid."); + return; + } + + CurrentVersion = Math.Min(preferredVersion, MaxVersion); + _log.LogInformation($"Protocol negotiation successed. Use protocol {CurrentVersion}"); + + if (message.Sender != null) + { + _log.LogInformation("Respond to protocol negotiation."); + message.Sender.Transmit(Message.FromPayload( + MessageTypes.ProtocolVersion, + 0, + new { Version = CurrentVersion })); + } + else + { + _log.LogInformation($"{nameof(Message.Sender)} is null."); + } + } + + private static int? GetProtocolVersionFromEnvironment() + { + // look for the environment variable DTH_PROTOCOL, if it is set override the protocol version. + // this is for debugging. + var strProtocol = Environment.GetEnvironmentVariable(EnvDthProtocol); + int intProtocol = -1; + if (!string.IsNullOrEmpty(strProtocol) && Int32.TryParse(strProtocol, out intProtocol)) + { + return intProtocol; + } + + return null; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel.Server/project.json b/src/Microsoft.DotNet.ProjectModel.Server/project.json new file mode 100644 index 000000000..0e5a4319d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel.Server/project.json @@ -0,0 +1,34 @@ +{ + "name": "dotnet-projectmodel-server", + "version": "1.0.0-*", + "compilationOptions": { + "emitEntryPoint": true + }, + "dependencies": { + "System.Console": "4.0.0-beta-23504", + "System.Collections": "4.0.11-beta-23504", + "System.Diagnostics.Process": "4.1.0-beta-23504", + "System.Linq": "4.0.1-beta-23504", + "System.Linq.Expressions": "4.0.11-beta-23504", + "System.Net.Sockets": "4.1.0-beta-23504", + "System.Runtime.Serialization.Primitives": "4.1.0-beta-23504", + "System.Threading.ThreadPool": "4.0.10-beta-23504", + "Microsoft.DotNet.ProjectModel": "1.0.0-*", + "Microsoft.Extensions.CommandLineUtils.Sources": { + "type": "build", + "version": "1.0.0-*" + }, + "Microsoft.Extensions.Logging": "1.0.0-*", + "Microsoft.Extensions.Logging.Console": "1.0.0-*", + "Newtonsoft.Json": "7.0.1" + }, + "frameworks": { + "dnxcore50": { } + }, + "scripts": { + "postcompile": [ + "../../scripts/build/place-binary \"%compile:OutputDir%/%project:Name%.dll\"", + "../../scripts/build/place-binary \"%compile:OutputDir%/%project:Name%.pdb\"" + ] + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/CommonCompilerOptions.cs b/src/Microsoft.DotNet.ProjectModel/CommonCompilerOptions.cs index 2f1555b00..7feabdea9 100644 --- a/src/Microsoft.DotNet.ProjectModel/CommonCompilerOptions.cs +++ b/src/Microsoft.DotNet.ProjectModel/CommonCompilerOptions.cs @@ -31,6 +31,28 @@ namespace Microsoft.DotNet.ProjectModel public bool? PreserveCompilationContext { get; set; } + public override bool Equals(object obj) + { + var other = obj as CommonCompilerOptions; + return other != null && + LanguageVersion == other.LanguageVersion && + Platform == other.Platform && + AllowUnsafe == other.AllowUnsafe && + WarningsAsErrors == other.WarningsAsErrors && + Optimize == other.Optimize && + KeyFile == other.KeyFile && + DelaySign == other.DelaySign && + PublicSign == other.PublicSign && + EmitEntryPoint == other.EmitEntryPoint && + PreserveCompilationContext == other.PreserveCompilationContext && + Enumerable.SequenceEqual(Defines ?? Enumerable.Empty(), other.Defines ?? Enumerable.Empty()); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + public static CommonCompilerOptions Combine(params CommonCompilerOptions[] options) { var result = new CommonCompilerOptions(); @@ -102,6 +124,5 @@ namespace Microsoft.DotNet.ProjectModel return result; } - } } diff --git a/src/Microsoft.DotNet.ProjectModel/Project.cs b/src/Microsoft.DotNet.ProjectModel/Project.cs index 60bfa6187..f884b3609 100644 --- a/src/Microsoft.DotNet.ProjectModel/Project.cs +++ b/src/Microsoft.DotNet.ProjectModel/Project.cs @@ -94,8 +94,7 @@ namespace Microsoft.DotNet.ProjectModel return _compilerOptionsByConfiguration.Keys; } - public CommonCompilerOptions GetCompilerOptions(NuGetFramework targetFramework, - string configurationName) + public CommonCompilerOptions GetCompilerOptions(NuGetFramework targetFramework, string configurationName) { // Get all project options and combine them var rootOptions = GetCompilerOptions(); diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectContextCollection.cs b/src/Microsoft.DotNet.ProjectModel/ProjectContextCollection.cs new file mode 100644 index 000000000..2654a8100 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/ProjectContextCollection.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.DotNet.ProjectModel +{ + public class ProjectContextCollection + { + public List ProjectContexts { get; } = new List(); + + public List ProjectDiagnostics { get; } = new List(); + + public string LockFilePath { get; set; } + + public string ProjectFilePath { get; set; } + + public DateTime LastProjectFileWriteTime { get; set; } + + public DateTime LastLockFileWriteTime { get; set; } + + public bool HasChanged + { + get + { + if (ProjectFilePath == null || !File.Exists(ProjectFilePath)) + { + return true; + } + + if (LastProjectFileWriteTime < File.GetLastWriteTime(ProjectFilePath)) + { + return true; + } + + if (LockFilePath == null || !File.Exists(LockFilePath)) + { + return true; + } + + if (LastLockFileWriteTime < File.GetLastWriteTime(LockFilePath)) + { + return true; + } + + return false; + } + } + + public void Reset() + { + ProjectContexts.Clear(); + ProjectFilePath = null; + LockFilePath = null; + LastLockFileWriteTime = DateTime.MinValue; + LastProjectFileWriteTime = DateTime.MinValue; + ProjectDiagnostics.Clear(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/Resolution/FrameworkReferenceResolver.cs b/src/Microsoft.DotNet.ProjectModel/Resolution/FrameworkReferenceResolver.cs index dc4a21ea4..24af7398d 100644 --- a/src/Microsoft.DotNet.ProjectModel/Resolution/FrameworkReferenceResolver.cs +++ b/src/Microsoft.DotNet.ProjectModel/Resolution/FrameworkReferenceResolver.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Runtime.Versioning; using System.Xml.Linq; +using Microsoft.DotNet.ProjectModel; using Microsoft.DotNet.ProjectModel.Utilities; using NuGet.Frameworks; @@ -18,6 +19,8 @@ namespace Microsoft.DotNet.ProjectModel.Resolution private static readonly NuGetFramework Dnx46 = new NuGetFramework( FrameworkConstants.FrameworkIdentifiers.Dnx, new Version(4, 6)); + + private static FrameworkReferenceResolver _default; private readonly IDictionary _cache = new Dictionary(); @@ -33,6 +36,19 @@ namespace Microsoft.DotNet.ProjectModel.Resolution } public string ReferenceAssembliesPath { get; } + + public static FrameworkReferenceResolver Default + { + get + { + if (_default == null) + { + _default = new FrameworkReferenceResolver(ProjectContextBuilder.GetDefaultReferenceAssembliesPath()); + } + + return _default; + } + } public bool TryGetAssembly(string name, NuGetFramework targetFramework, out string path, out Version version) { diff --git a/src/Microsoft.DotNet.ProjectModel/WorkspaceContext.cs b/src/Microsoft.DotNet.ProjectModel/WorkspaceContext.cs index 9bf03e3ba..1ea288ee4 100644 --- a/src/Microsoft.DotNet.ProjectModel/WorkspaceContext.cs +++ b/src/Microsoft.DotNet.ProjectModel/WorkspaceContext.cs @@ -21,17 +21,15 @@ namespace Microsoft.DotNet.ProjectModel = new ConcurrentDictionary>(); // key: project directory, target framework - private readonly ConcurrentDictionary _projectContextsCache - = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _projectContextsCache + = new ConcurrentDictionary(); private readonly HashSet _projects = new HashSet(StringComparer.OrdinalIgnoreCase); private bool _needRefresh; - private WorkspaceContext(List projectPaths, string configuration) + private WorkspaceContext(IEnumerable projectPaths) { - Configuration = configuration; - foreach (var path in projectPaths) { AddProject(path); @@ -40,8 +38,6 @@ namespace Microsoft.DotNet.ProjectModel Refresh(); } - public string Configuration { get; } - /// /// Create a WorkspaceContext from a given path. /// @@ -54,7 +50,7 @@ namespace Microsoft.DotNet.ProjectModel /// If the given path points to a project.json, all the projects it referenced as well as itself /// are added to the WorkspaceContext. /// - public static WorkspaceContext CreateFrom(string projectPath, string configuration) + public static WorkspaceContext CreateFrom(string projectPath) { var projectPaths = ResolveProjectPath(projectPath); if (projectPaths == null || !projectPaths.Any()) @@ -62,13 +58,13 @@ namespace Microsoft.DotNet.ProjectModel return null; } - var context = new WorkspaceContext(projectPaths, configuration); + var context = new WorkspaceContext(projectPaths); return context; } - public static WorkspaceContext CreateFrom(string projectPath) + public static WorkspaceContext Create() { - return CreateFrom(projectPath, "Debug"); + return new WorkspaceContext(Enumerable.Empty()); } public void AddProject(string path) @@ -107,7 +103,7 @@ namespace Microsoft.DotNet.ProjectModel foreach (var projectDirectory in basePaths) { - var project = GetProject(projectDirectory); + var project = GetProject(projectDirectory).Model; if (project == null) { continue; @@ -119,7 +115,7 @@ namespace Microsoft.DotNet.ProjectModel { foreach (var reference in GetProjectReferences(projectContext)) { - var referencedProject = GetProject(reference.Path); + var referencedProject = GetProject(reference.Path).Model; if (referencedProject != null) { _projects.Add(referencedProject.ProjectDirectory); @@ -132,19 +128,24 @@ namespace Microsoft.DotNet.ProjectModel } public IReadOnlyList GetProjectContexts(string projectPath) + { + return GetProjectContextCollection(projectPath).ProjectContexts; + } + + public ProjectContextCollection GetProjectContextCollection(string projectPath) { return _projectContextsCache.AddOrUpdate( projectPath, key => AddProjectContextEntry(key, null), - (key, oldEntry) => AddProjectContextEntry(key, oldEntry)).ProjectContexts; + (key, oldEntry) => AddProjectContextEntry(key, oldEntry)); } - private Project GetProject(string projectDirectory) + private FileModelEntry GetProject(string projectDirectory) { return _projectsCache.AddOrUpdate( projectDirectory, key => AddProjectEntry(key, null), - (key, oldEntry) => AddProjectEntry(key, oldEntry)).Model; + (key, oldEntry) => AddProjectEntry(key, oldEntry)); } private LockFile GetLockFile(string projectDirectory) @@ -171,7 +172,7 @@ namespace Microsoft.DotNet.ProjectModel if (currentEntry.IsInvalid) { Project project; - if (!ProjectReader.TryGetProject(projectDirectory, out project)) + if (!ProjectReader.TryGetProject(projectDirectory, out project, currentEntry.Diagnostics)) { currentEntry.Reset(); } @@ -208,23 +209,24 @@ namespace Microsoft.DotNet.ProjectModel return currentEntry; } - private ProjectContextEntry AddProjectContextEntry(string projectDirectory, - ProjectContextEntry currentEntry) + private ProjectContextCollection AddProjectContextEntry(string projectDirectory, + ProjectContextCollection currentEntry) { if (currentEntry == null) { // new entry required - currentEntry = new ProjectContextEntry(); + currentEntry = new ProjectContextCollection(); } - var project = GetProject(projectDirectory); - if (project == null) + var projectEntry = GetProject(projectDirectory); + if (projectEntry.Model == null) { // project doesn't exist anymore currentEntry.Reset(); return currentEntry; } + var project = projectEntry.Model; if (currentEntry.HasChanged) { currentEntry.Reset(); @@ -232,7 +234,7 @@ namespace Microsoft.DotNet.ProjectModel foreach (var framework in project.GetTargetFrameworks()) { var builder = new ProjectContextBuilder() - .WithProjectResolver(path => GetProject(path)) + .WithProjectResolver(path => GetProject(path).Model) .WithLockFileResolver(path => GetLockFile(path)) .WithProject(project) .WithTargetFramework(framework.FrameworkName); @@ -249,6 +251,8 @@ namespace Microsoft.DotNet.ProjectModel currentEntry.LockFilePath = lockFilePath; currentEntry.LastLockFileWriteTime = File.GetLastWriteTime(lockFilePath); } + + currentEntry.ProjectDiagnostics.AddRange(projectEntry.Diagnostics); } return currentEntry; @@ -262,6 +266,8 @@ namespace Microsoft.DotNet.ProjectModel public string FilePath { get; set; } + public List Diagnostics { get; } = new List(); + public void UpdateLastWriteTime() { _lastWriteTime = File.GetLastWriteTime(FilePath); @@ -289,60 +295,11 @@ namespace Microsoft.DotNet.ProjectModel { Model = null; FilePath = null; + Diagnostics.Clear(); _lastWriteTime = DateTime.MinValue; } } - private class ProjectContextEntry - { - public List ProjectContexts { get; } = new List(); - - public string LockFilePath { get; set; } - - public string ProjectFilePath { get; set; } - - public DateTime LastProjectFileWriteTime { get; set; } - - public DateTime LastLockFileWriteTime { get; set; } - - public bool HasChanged - { - get - { - if (ProjectFilePath == null || !File.Exists(ProjectFilePath)) - { - return true; - } - - if (LastProjectFileWriteTime < File.GetLastWriteTime(ProjectFilePath)) - { - return true; - } - - if (LockFilePath == null || !File.Exists(LockFilePath)) - { - return true; - } - - if (LastLockFileWriteTime < File.GetLastWriteTime(LockFilePath)) - { - return true; - } - - return false; - } - } - - public void Reset() - { - ProjectContexts.Clear(); - ProjectFilePath = null; - LockFilePath = null; - LastLockFileWriteTime = DateTime.MinValue; - LastProjectFileWriteTime = DateTime.MinValue; - } - } - private static string NormalizeProjectPath(string path) { if (File.Exists(path) && diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthStartupTests.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthStartupTests.cs new file mode 100644 index 000000000..f8250182a --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthStartupTests.cs @@ -0,0 +1,262 @@ +// Copyright (c) .NET Foundation. 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.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Server.Tests.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests +{ + public class DthStartupTests : IClassFixture + { + private readonly TestHelper _testHelper; + + public DthStartupTests(TestHelper helper) + { + _testHelper = helper; + } + + [Fact] + public void DthStartup_GetProjectInformation() + { + var projectPath = _testHelper.FindSampleProject("EmptyConsoleApp"); + Assert.NotNull(projectPath); + + using (var server = new DthTestServer(_testHelper.LoggerFactory)) + using (var client = new DthTestClient(server)) + { + client.Initialize(projectPath); + + var projectInformation = client.DrainTillFirst(MessageTypes.ProjectInformation) + .EnsureSource(server, client) + .RetrievePayloadAs() + .AssertProperty("Name", "EmptyConsoleApp"); + + projectInformation.RetrievePropertyAs("Configurations") + .AssertJArrayCount(2) + .AssertJArrayContains("Debug") + .AssertJArrayContains("Release"); + + var frameworkShortNames = projectInformation.RetrievePropertyAs("Frameworks") + .AssertJArrayCount(2) + .Select(f => f["ShortName"].Value()); + + Assert.Contains("dnxcore50", frameworkShortNames); + Assert.Contains("dnx451", frameworkShortNames); + } + } + + [Theory] + [InlineData(4, 4)] + [InlineData(5, 4)] + [InlineData(3, 3)] + public void DthStartup_ProtocolNegotiation(int requestVersion, int expectVersion) + { + using (var server = new DthTestServer(_testHelper.LoggerFactory)) + using (var client = new DthTestClient(server)) + { + client.SetProtocolVersion(requestVersion); + + var response = client.DrainTillFirst(MessageTypes.ProtocolVersion, TimeSpan.FromDays(1)); + response.EnsureSource(server, client); + + Assert.Equal(expectVersion, response.Payload["Version"]?.Value()); + } + } + + [Theory] + public void DthStartup_ProtocolNegotiation_ZeroIsNoAllowed() + { + using (var server = new DthTestServer(_testHelper.LoggerFactory)) + using (var client = new DthTestClient(server)) + { + client.SetProtocolVersion(0); + + Assert.Throws(() => + { + client.DrainTillFirst(MessageTypes.ProtocolVersion, timeout: TimeSpan.FromSeconds(1)); + }); + } + } + + [Fact] + public void DthCompilation_GetDiagnostics_OnEmptyConsoleApp() + { + var projectPath = _testHelper.FindSampleProject("EmptyConsoleApp"); + Assert.NotNull(projectPath); + + using (var server = new DthTestServer(_testHelper.LoggerFactory)) + using (var client = new DthTestClient(server)) + { + // Drain the inital messages + client.Initialize(projectPath); + client.SendPayLoad(projectPath, "GetDiagnostics"); + + var diagnosticsGroup = client.DrainTillFirst("AllDiagnostics") + .EnsureSource(server, client) + .RetrievePayloadAs() + .AssertJArrayCount(3); + + foreach (var group in diagnosticsGroup) + { + group.AsJObject() + .AssertProperty("Errors", errorsArray => !errorsArray.Any()) + .AssertProperty("Warnings", warningsArray => !warningsArray.Any()); + } + } + } + + [Theory] + [InlineData("Project", "UnresolvedProjectSample", "EmptyLibrary", "Project")] + [InlineData("Package", "UnresolvedPackageSample", "NoSuchPackage", null)] + [InlineData("Package", "IncompatiblePackageSample", "Newtonsoft.Json", "Package")] + public void DthCompilation_Initialize_UnresolvedDependency(string referenceType, + string testProjectName, + string expectedUnresolvedDependency, + string expectedUnresolvedType) + { + var projectPath = _testHelper.FindSampleProject(testProjectName); + Assert.NotNull(projectPath); + + using (var server = new DthTestServer(_testHelper.LoggerFactory)) + using (var client = new DthTestClient(server)) + { + client.Initialize(projectPath); + + var unresolveDependency = client.DrainTillFirst("Dependencies") + .EnsureSource(server, client) + .RetrieveDependency(expectedUnresolvedDependency); + + unresolveDependency.AssertProperty("Name", expectedUnresolvedDependency) + .AssertProperty("DisplayName", expectedUnresolvedDependency) + .AssertProperty("Resolved", false) + .AssertProperty("Type", expectedUnresolvedType); + + if (expectedUnresolvedType == "Project") + { + unresolveDependency.AssertProperty("Path", Path.Combine(Path.GetDirectoryName(projectPath), + expectedUnresolvedDependency, + Project.FileName)); + } + else + { + Assert.False(unresolveDependency["Path"].HasValues); + } + + var referencesMessage = client.DrainTillFirst("References", TimeSpan.FromDays(1)) + .EnsureSource(server, client); + + if (referenceType == "Project") + { + var expectedUnresolvedProjectPath = Path.Combine(Path.GetDirectoryName(projectPath), + expectedUnresolvedDependency, + Project.FileName); + + referencesMessage.RetrievePayloadAs() + .RetrievePropertyAs("ProjectReferences") + .AssertJArrayCount(1) + .RetrieveArraryElementAs(0) + .AssertProperty("Name", expectedUnresolvedDependency) + .AssertProperty("Path", expectedUnresolvedProjectPath) + .AssertProperty("WrappedProjectPath", prop => !prop.HasValues); + } + else if (referenceType == "Package") + { + referencesMessage.RetrievePayloadAs() + .RetrievePropertyAs("ProjectReferences") + .AssertJArrayCount(0); + } + } + } + + [Fact] + public void DthNegative_BrokenProjectPathInLockFile() + { + using (var server = new DthTestServer(_testHelper.LoggerFactory)) + using (var client = new DthTestClient(server)) + { + // After restore the project is copied to another place so that + // the relative path in project lock file is invalid. + var movedProjectPath = _testHelper.MoveProject("BrokenProjectPathSample"); + + client.Initialize(movedProjectPath); + + client.DrainTillFirst("DependencyDiagnostics") + .RetrieveDependencyDiagnosticsCollection() + .RetrieveDependencyDiagnosticsErrorAt(0) + .AssertProperty("FormattedMessage", message => message.Contains("error NU1002")) + .RetrievePropertyAs("Source") + .AssertProperty("Name", "EmptyLibrary"); + + client.DrainTillFirst("Dependencies") + .RetrieveDependency("EmptyLibrary") + .AssertProperty("Errors", errorsArray => errorsArray.Count == 1) + .AssertProperty("Warnings", warningsArray => warningsArray.Count == 0) + .AssertProperty("Name", "EmptyLibrary") + .AssertProperty("Resolved", false); + } + } + + [Fact(Skip = "Require dotnet restore integration test")] + public void DthDependencies_UpdateGlobalJson_RefreshDependencies() + { + var projectPath = _testHelper.CreateSampleProject("DthUpdateSearchPathSample"); + Assert.True(Directory.Exists(projectPath)); + + using (var server = new DthTestServer(_testHelper.LoggerFactory)) + using (var client = new DthTestClient(server)) + { + var testProject = Path.Combine(projectPath, "home", "src", "MainProject"); + + client.Initialize(testProject); + + client.DrainTillFirst("ProjectInformation") + .RetrievePayloadAs() + .RetrievePropertyAs("ProjectSearchPaths") + .AssertJArrayCount(2); + + client.DrainTillFirst("DependencyDiagnostics") + .RetrievePayloadAs() + .AssertProperty("Errors", array => array.Count == 0) + .AssertProperty("Warnings", array => array.Count == 0); + + client.DrainTillFirst("Dependencies") + .RetrieveDependency("Newtonsoft.Json") + .AssertProperty("Type", "Project") + .AssertProperty("Resolved", true) + .AssertProperty("Errors", array => array.Count == 0, _ => "Dependency shouldn't contain any error."); + + // Overwrite the global.json to remove search path to ext + File.WriteAllText( + Path.Combine(projectPath, "home", GlobalSettings.FileName), + JsonConvert.SerializeObject(new { project = new string[] { "src" } })); + + client.SendPayLoad(testProject, "RefreshDependencies"); + + client.DrainTillFirst("ProjectInformation") + .RetrievePayloadAs() + .RetrievePropertyAs("ProjectSearchPaths") + .AssertJArrayCount(1) + .AssertJArrayElement(0, Path.Combine(projectPath, "home", "src")); + + client.DrainTillFirst("DependencyDiagnostics") + .RetrieveDependencyDiagnosticsCollection() + .RetrieveDependencyDiagnosticsErrorAt(0) + .AssertProperty("ErrorCode", "NU1010"); + + client.DrainTillFirst("Dependencies") + .RetrieveDependency("Newtonsoft.Json") + .AssertProperty("Type", "") + .AssertProperty("Resolved", false) + .RetrievePropertyAs("Errors") + .AssertJArrayCount(1) + .RetrieveArraryElementAs(0) + .AssertProperty("ErrorCode", "NU1010"); + } + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthTestClient.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthTestClient.cs new file mode 100644 index 000000000..0b21015b9 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthTestClient.cs @@ -0,0 +1,273 @@ +// Copyright (c) .NET Foundation. 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.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests +{ + public class DthTestClient : IDisposable + { + private readonly string _hostId; + private readonly BinaryReader _reader; + private readonly BinaryWriter _writer; + private readonly NetworkStream _networkStream; + + private readonly BlockingCollection _messageQueue; + private readonly CancellationTokenSource _readCancellationToken; + + // Keeps track of initialized project contexts + // REVIEW: This needs to be exposed if we ever create 2 clients in order to simulate how build + // works in visual studio + private readonly Dictionary _projectContexts = new Dictionary(); + private int _nextContextId; + + public DthTestClient(DthTestServer server) + { + var socket = new Socket(AddressFamily.InterNetwork, + SocketType.Stream, + ProtocolType.Tcp); + + socket.Connect(new IPEndPoint(IPAddress.Loopback, server.Port)); + + _hostId = server.HostId; + + _networkStream = new NetworkStream(socket); + _reader = new BinaryReader(_networkStream); + _writer = new BinaryWriter(_networkStream); + + _messageQueue = new BlockingCollection(); + + _readCancellationToken = new CancellationTokenSource(); + Task.Run(() => ReadMessage(_readCancellationToken.Token), _readCancellationToken.Token); + } + + public void SendPayLoad(Project project, string messageType) + { + SendPayLoad(project.ProjectDirectory, messageType); + } + + public void SendPayLoad(string projectPath, string messageType) + { + int contextId; + if (!_projectContexts.TryGetValue(projectPath, out contextId)) + { + Assert.True(false, $"Unable to resolve context for {projectPath}"); + } + + SendPayLoad(contextId, messageType); + } + + public void SendPayLoad(int contextId, string messageType) + { + SendPayLoad(contextId, messageType, new { }); + } + + public void SendPayLoad(int contextId, string messageType, object payload) + { + lock (_writer) + { + var message = new + { + ContextId = contextId, + HostId = _hostId, + MessageType = messageType, + Payload = payload + }; + _writer.Write(JsonConvert.SerializeObject(message)); + } + } + + public int Initialize(string projectPath) + { + var contextId = _nextContextId++; + + _projectContexts[projectPath] = contextId; + SendPayLoad(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath }); + + return contextId; + } + + public int Initialize(string projectPath, int protocolVersion) + { + var contextId = _nextContextId++; + + _projectContexts[projectPath] = contextId; + SendPayLoad(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath, Version = protocolVersion }); + + return contextId; + } + + public int Initialize(string projectPath, int protocolVersion, string configuration) + { + var contextId = _nextContextId++; + + _projectContexts[projectPath] = contextId; + SendPayLoad(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath, Version = protocolVersion, Configuration = configuration }); + + return contextId; + } + + public void SetProtocolVersion(int version) + { + SendPayLoad(0, MessageTypes.ProtocolVersion, new { Version = version }); + } + + public List DrainMessage(int count) + { + var result = new List(); + while (count > 0) + { + result.Add(GetResponse(timeout: TimeSpan.FromSeconds(10))); + count--; + } + + return result; + } + + public List DrainAllMessages() + { + return DrainAllMessages(TimeSpan.FromSeconds(10)); + } + + /// + /// Read all messages from pipeline till timeout + /// + /// The timeout + /// All the messages in a list + public List DrainAllMessages(TimeSpan timeout) + { + var result = new List(); + while (true) + { + try + { + result.Add(GetResponse(timeout)); + } + catch (TimeoutException) + { + return result; + } + catch (Exception) + { + throw; + } + } + } + + /// + /// Read messages from pipeline until the first match + /// ] + /// A message type + /// The first match message + public DthMessage DrainTillFirst(string type) + { + return DrainTillFirst(type, TimeSpan.FromSeconds(10)); + } + + /// + /// Read messages from pipeline until the first match + /// + /// A message type + /// Timeout for each read + /// The first match message + public DthMessage DrainTillFirst(string type, TimeSpan timeout) + { + while (true) + { + var next = GetResponse(timeout); + if (next.MessageType == type) + { + return next; + } + } + } + + /// + /// Read messages from pipeline until the first match + /// + /// A message type + /// Timeout + /// All the messages read before the first match + /// The first match + public DthMessage DrainTillFirst(string type, TimeSpan timeout, out List leadingMessages) + { + leadingMessages = new List(); + while (true) + { + var next = GetResponse(timeout); + if (next.MessageType == type) + { + return next; + } + else + { + leadingMessages.Add(next); + } + } + } + + public void Dispose() + { + _reader.Dispose(); + _writer.Dispose(); + _networkStream.Dispose(); + _readCancellationToken.Cancel(); + } + + private void ReadMessage(CancellationToken cancellationToken) + { + while (true) + { + try + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var content = _reader.ReadString(); + var message = JsonConvert.DeserializeObject(content); + + _messageQueue.Add(message); + } + catch (IOException) + { + // swallow + } + catch (JsonSerializationException deserializException) + { + throw new InvalidOperationException( + $"Fail to deserailze data into {nameof(DthMessage)}.", + deserializException); + } + catch (Exception ex) + { + throw ex; + } + } + } + + private DthMessage GetResponse(TimeSpan timeout) + { + DthMessage message; + + if (_messageQueue.TryTake(out message, timeout)) + { + return message; + } + else + { + throw new TimeoutException($"Response time out after {timeout.TotalSeconds} seconds."); + } + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthTestServer.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthTestServer.cs new file mode 100644 index 000000000..175ee2dad --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/DthTestServer.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. 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.Net; +using System.Net.Sockets; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests +{ + public class DthTestServer : IDisposable + { + private readonly Program _program; + private readonly Thread _thread; + + public DthTestServer(ILoggerFactory loggerFactory) + { + LoggerFactory = loggerFactory; + + Port = FindFreePort(); + HostId = Guid.NewGuid().ToString(); + + _program = new Program(Port, HostId, LoggerFactory); + + _thread = new Thread(() => { _program.OpenChannel(); }); + _thread.Start(); + } + + public string HostId { get; } + + public int Port { get; } + + public ILoggerFactory LoggerFactory { get; } + + public void Dispose() + { + try + { + _program.Shutdown(); + } + catch (InvalidOperationException) + { + // swallow the exception if the process had been terminated. + } + } + + private static int FindFreePort() + { + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)socket.LocalEndPoint).Port; + } + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessage.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessage.cs new file mode 100644 index 000000000..c0a4eee22 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessage.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests +{ + public class DthMessage + { + public string HostId { get; set; } + + public string MessageType { get; set; } + + public int ContextId { get; set; } + + public int Version { get; set; } + + public JToken Payload { get; set; } + + // for ProjectContexts message only + public Dictionary Projects { get; set; } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessageCollectionExtension.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessageCollectionExtension.cs new file mode 100644 index 000000000..e2f0c4dde --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessageCollectionExtension.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. 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.Linq; +using System.Runtime.Versioning; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests +{ + public static class DthMessageCollectionExtension + { + public static IList GetMessagesByFramework(this IEnumerable messages, FrameworkName targetFramework) + { + return messages.Where(msg => MatchesFramework(targetFramework, msg)).ToList(); + } + + public static IList GetMessagesByType(this IEnumerable messages, string typename) + { + return messages.Where(msg => string.Equals(msg.MessageType, typename)).ToList(); + } + + public static DthMessage RetrieveSingleMessage(this IEnumerable messages, + string typename) + { + var result = messages.SingleOrDefault(msg => string.Equals(msg.MessageType, typename, StringComparison.Ordinal)); + + if (result == null) + { + if (messages.FirstOrDefault(msg => string.Equals(msg.MessageType, typename, StringComparison.Ordinal)) != null) + { + Assert.False(true, $"More than one {typename} messages exist."); + } + else + { + Assert.False(true, $"{typename} message doesn't exists."); + } + } + + return result; + } + + public static IEnumerable ContainsMessage(this IEnumerable messages, + string typename) + { + var contain = messages.FirstOrDefault(msg => string.Equals(msg.MessageType, typename, StringComparison.Ordinal)) != null; + + Assert.True(contain, $"Messages collection doesn't contain message of type {typename}."); + + return messages; + } + + public static IEnumerable AssertDoesNotContain(this IEnumerable messages, string typename) + { + var notContain = messages.FirstOrDefault(msg => string.Equals(msg.MessageType, typename, StringComparison.Ordinal)) == null; + + Assert.True(notContain, $"Message collection contains message of type {typename}."); + + return messages; + } + + private static bool MatchesFramework(FrameworkName targetFramework, DthMessage msg) + { + if (msg.Payload.Type != JTokenType.Object) + { + return false; + } + + var frameworkObj = msg.Payload["Framework"]; + + if (frameworkObj == null || !frameworkObj.HasValues) + { + return false; + } + + return string.Equals(frameworkObj.Value("FrameworkName"), targetFramework.FullName, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessageExtension.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessageExtension.cs new file mode 100644 index 000000000..fd8190756 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/DthMessageExtension.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests +{ + public static class DthMessageExtension + { + public static JObject RetrieveDependency(this DthMessage message, string dependencyName) + { + Assert.NotNull(message); + Assert.Equal(MessageTypes.Dependencies, message.MessageType); + + var payload = message.Payload as JObject; + Assert.NotNull(payload); + + var dependency = payload[MessageTypes.Dependencies][dependencyName] as JObject; + Assert.NotNull(dependency); + Assert.Equal(dependencyName, dependency["Name"].Value()); + + return dependency; + } + + public static DthMessage EnsureNotContainDependency(this DthMessage message, string dependencyName) + { + Assert.NotNull(message); + Assert.Equal(MessageTypes.Dependencies, message.MessageType); + + var payload = message.Payload as JObject; + Assert.NotNull(payload); + + Assert.True(payload[MessageTypes.Dependencies][dependencyName] == null, $"Unexpected dependency {dependencyName} exists."); + + return message; + } + + public static JObject RetrieveDependencyDiagnosticsCollection(this DthMessage message) + { + Assert.NotNull(message); + Assert.Equal(MessageTypes.DependencyDiagnostics, message.MessageType); + + var payload = message.Payload as JObject; + Assert.NotNull(payload); + + return payload; + } + + public static JObject RetrieveCompilationDiagnostics(this DthMessage message, string frameworkShortName) + { + Assert.NotNull(message); + Assert.Equal(MessageTypes.AllDiagnostics, message.MessageType); + + Assert.True(message.Payload is JArray); + var payload = (JArray)message.Payload; + + foreach (var each in payload) + { + Assert.True(each is JObject); + var diagnosticsOfFramework = (JObject)each; + + if (string.Equals(diagnosticsOfFramework["Framework"]["ShortName"].Value(), + frameworkShortName, + StringComparison.OrdinalIgnoreCase)) + { + return diagnosticsOfFramework; + } + } + + return null; + } + + public static T RetrievePayloadAs(this DthMessage message) + where T : JToken + { + Assert.NotNull(message); + AssertType(message.Payload, "Payload"); + + return (T)message.Payload; + } + + /// + /// Throws if the message is not generated in communication between given server and client + /// + public static DthMessage EnsureSource(this DthMessage message, DthTestServer server, DthTestClient client) + { + if (message.HostId != server.HostId) + { + throw new Exception($"{nameof(message.HostId)} doesn't match the one of server. Expected {server.HostId} but actually {message.HostId}."); + } + + return message; + } + + public static void AssertType(object obj, string name) + { + Assert.True(obj is T, $"{name} is not of type {typeof(T).Name}."); + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/JArrayExtensions.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/JArrayExtensions.cs new file mode 100644 index 000000000..f7cddb8bb --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/JArrayExtensions.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests +{ + public static class JArrayExtensions + { + public static JArray AssertJArrayEmpty(this JArray array) + { + Assert.NotNull(array); + Assert.Empty(array); + + return array; + } + + public static JArray AssertJArrayNotEmpty(this JArray array) + { + Assert.NotNull(array); + Assert.NotEmpty(array); + + return array; + } + + public static JArray AssertJArrayCount(this JArray array, int expectedCount) + { + Assert.NotNull(array); + Assert.Equal(expectedCount, array.Count); + + return array; + } + + public static JArray AssertJArrayElement(this JArray array, int index, T expectedElementValue) + { + Assert.NotNull(array); + + var element = array[index]; + Assert.NotNull(element); + Assert.Equal(expectedElementValue, element.Value()); + + return array; + } + + public static JArray AssertJArrayContains(this JArray array, T value) + { + AssertJArrayContains(array, element => object.Equals(element, value)); + + return array; + } + + public static JArray AssertJArrayContains(this JArray array, Func critiera) + { + bool contains = false; + foreach (var element in array) + { + var value = element.Value(); + + contains = critiera(value); + if (contains) + { + break; + } + } + + Assert.True(contains, "JArray doesn't contains the specified element."); + + return array; + } + + public static T RetrieveArraryElementAs(this JArray json, int index) + where T : JToken + { + Assert.NotNull(json); + Assert.True(index >= 0 && index < json.Count, "Index out of range"); + + var element = json[index]; + DthMessageExtension.AssertType(element, $"Element at {index}"); + + return (T)element; + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/JObjectExtensions.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/JObjectExtensions.cs new file mode 100644 index 000000000..4d944ac79 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/JObjectExtensions.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests +{ + public static class JObjectExtensions + { + public static JObject AsJObject(this JToken token) + { + DthMessageExtension.AssertType(token, nameof(JToken)); + + return (JObject)token; + } + + public static JObject RetrieveDependencyDiagnosticsErrorAt(this JObject payload, int index) + { + Assert.NotNull(payload); + + return payload.RetrievePropertyAs("Errors") + .RetrieveArraryElementAs(index); + } + + public static T RetrieveDependencyDiagnosticsErrorAt(this JObject payload, int index) + where T : JToken + { + Assert.NotNull(payload); + + return payload.RetrievePropertyAs("Errors") + .RetrieveArraryElementAs(index); + } + + public static T RetrievePropertyAs(this JObject json, string propertyName) + where T : JToken + { + Assert.NotNull(json); + + var property = json[propertyName]; + Assert.NotNull(property); + DthMessageExtension.AssertType(property, $"Property {propertyName}"); + + return (T)property; + } + + public static JObject AssertProperty(this JObject json, string propertyName, T expectedPropertyValue) + { + Assert.NotNull(json); + + var property = json[propertyName]; + Assert.NotNull(property); + Assert.Equal(expectedPropertyValue, property.Value()); + + return json; + } + + public static JObject AssertProperty(this JObject json, string propertyName, Func assertion) + { + return AssertProperty(json, + propertyName, + assertion, + value => $"Assert failed on {propertyName}."); + } + + public static JObject AssertProperty(this JObject json, string propertyName, Func assertion, Func errorMessage) + { + Assert.NotNull(json); + + var property = json[propertyName]; + Assert.False(property == null, $"Property {propertyName} doesn't exist."); + + var propertyValue = property.Value(); + Assert.False(propertyValue == null, $"Property {propertyName} of type {typeof(T).Name} doesn't exist."); + + Assert.True(assertion(propertyValue), + errorMessage(propertyValue)); + + return json; + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/TestHelper.cs b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/TestHelper.cs new file mode 100644 index 000000000..2907649f1 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Helpers/TestHelper.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.ProjectModel.Server.Tests.Helpers +{ + public class TestHelper + { + private readonly string _tempPath; + + public TestHelper() + { + LoggerFactory = new LoggerFactory(); + + var testVerbose = Environment.GetEnvironmentVariable("DOTNET_TEST_VERBOSE"); + if (testVerbose == "2") + { + LoggerFactory.AddConsole(LogLevel.Trace); + } + else if (testVerbose == "1") + { + LoggerFactory.AddConsole(LogLevel.Information); + } + else + { + LoggerFactory.AddConsole(LogLevel.Warning); + } + + _tempPath = CreateTempFolder(); + var dthTestProjectsFolder = Path.Combine(FindRoot(), "testapp", "DthTestProjects"); + CopyFiles(dthTestProjectsFolder, _tempPath); + + var logger = LoggerFactory.CreateLogger(); + logger.LogInformation($"Test projects are copied to {_tempPath}"); + } + + public ILoggerFactory LoggerFactory { get; } + + public string FindSampleProject(string name) + { + var result = Path.Combine(_tempPath, "src", name); + if (Directory.Exists(result)) + { + return result; + } + else + { + return null; + } + } + + public string CreateSampleProject(string name) + { + var source = Path.Combine(FindRoot(), "test", name); + if (!Directory.Exists(source)) + { + return null; + } + + var target = Path.Combine(CreateTempFolder(), name); + CopyFiles(source, target); + + return target; + } + + public string MoveProject(string projectName) + { + var projectPath = FindSampleProject(projectName); + var movedProjectPath = Path.Combine(CreateTempFolder(), projectName); + CopyFiles(projectPath, movedProjectPath); + + return movedProjectPath; + } + + private static string FindRoot() + { + var solutionName = "Microsoft.DotNet.Cli.sln"; + var root = new DirectoryInfo(Directory.GetCurrentDirectory()); + + while (root != null && root.GetFiles(solutionName).Length == 0) + { + root = Directory.GetParent(root.FullName); + } + + if (root != null) + { + return root.FullName; + } + else + { + return null; + } + } + + private static string CreateTempFolder() + { + var result = Path.GetTempFileName(); + File.Delete(result); + Directory.CreateDirectory(result); + + return result; + } + + private static void CopyFiles(string sourceFolder, string targetFolder) + { + if (!Directory.Exists(targetFolder)) + { + Directory.CreateDirectory(targetFolder); + } + + foreach (var filePath in Directory.EnumerateFiles(sourceFolder)) + { + var filename = Path.GetFileName(filePath); + File.Copy(filePath, Path.Combine(targetFolder, filename)); + } + + foreach (var folderPath in Directory.EnumerateDirectories(sourceFolder)) + { + var folderName = new DirectoryInfo(folderPath).Name; + CopyFiles(folderPath, Path.Combine(targetFolder, folderName)); + } + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/Microsoft.DotNet.ProjectModel.Server.Tests.xproj b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Microsoft.DotNet.ProjectModel.Server.Tests.xproj new file mode 100644 index 000000000..e249a5f52 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/Microsoft.DotNet.ProjectModel.Server.Tests.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 11c77123-e4da-499f-8900-80c88c2c69f2 + Microsoft.DotNet.ProjectModel.Server.Tests + ..\artifacts\obj\$(MSBuildProjectName) + ..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Server.Tests/project.json b/test/Microsoft.DotNet.ProjectModel.Server.Tests/project.json new file mode 100644 index 000000000..9c4a49c91 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Server.Tests/project.json @@ -0,0 +1,15 @@ +{ + "dependencies": { + "System.Dynamic.Runtime": "4.0.11-*", + "Microsoft.DotNet.ProjectModel": "1.0.0-*", + "Microsoft.DotNet.ProjectModel.Server": "1.0.0-*", + "Newtonsoft.Json": "7.0.1", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "frameworks": { + "dnxcore50": { } + }, + "commands": { + "test": "xunit.runner.aspnet" + } +} diff --git a/testapp/DthTestProjects/global.json b/testapp/DthTestProjects/global.json new file mode 100644 index 000000000..364730368 --- /dev/null +++ b/testapp/DthTestProjects/global.json @@ -0,0 +1,3 @@ +{ + "projects": ["src"] +} diff --git a/testapp/DthTestProjects/src/BrokenProjectPathSample/project.json b/testapp/DthTestProjects/src/BrokenProjectPathSample/project.json new file mode 100644 index 000000000..d94050821 --- /dev/null +++ b/testapp/DthTestProjects/src/BrokenProjectPathSample/project.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "EmptyLibrary": "" + }, + "frameworks": { + "dnxcore50": { } + } +} diff --git a/testapp/DthTestProjects/src/EmptyConsoleApp/Program.cs b/testapp/DthTestProjects/src/EmptyConsoleApp/Program.cs new file mode 100644 index 000000000..6c7859b64 --- /dev/null +++ b/testapp/DthTestProjects/src/EmptyConsoleApp/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace Misc.DthTestProjects.EmptyConsoleApp +{ + public class Program + { + public int Main(string[] args) + { + Console.WriteLine("Hello, world."); + return 0; + } + } +} diff --git a/testapp/DthTestProjects/src/EmptyConsoleApp/project.json b/testapp/DthTestProjects/src/EmptyConsoleApp/project.json new file mode 100644 index 000000000..dca862e43 --- /dev/null +++ b/testapp/DthTestProjects/src/EmptyConsoleApp/project.json @@ -0,0 +1,12 @@ +{ + "dependencies": { }, + "frameworks": { + "dnxcore50": { + "dependencies": { + "System.Runtime": "4.0.21-*", + "System.Console": "4.0.0-*" + } + }, + "dnx451": { } + } +} diff --git a/testapp/DthTestProjects/src/EmptyLibrary/Class.cs b/testapp/DthTestProjects/src/EmptyLibrary/Class.cs new file mode 100644 index 000000000..3fe898a95 --- /dev/null +++ b/testapp/DthTestProjects/src/EmptyLibrary/Class.cs @@ -0,0 +1,8 @@ +using System; + +namespace Misc.DthTestProjects.EmptyLibrary +{ + public class Class + { + } +} diff --git a/testapp/DthTestProjects/src/EmptyLibrary/project-update.json b/testapp/DthTestProjects/src/EmptyLibrary/project-update.json new file mode 100644 index 000000000..9aaa9064d --- /dev/null +++ b/testapp/DthTestProjects/src/EmptyLibrary/project-update.json @@ -0,0 +1,11 @@ +{ + "dependencies": { }, + "frameworks": { + "dnxcore50": { + "dependencies":{ + "System.Runtime": "4.0.21-beta-*", + "System.Console": "4.0.0-beta-*" + } + } + } +} diff --git a/testapp/DthTestProjects/src/EmptyLibrary/project.json b/testapp/DthTestProjects/src/EmptyLibrary/project.json new file mode 100644 index 000000000..2a399584b --- /dev/null +++ b/testapp/DthTestProjects/src/EmptyLibrary/project.json @@ -0,0 +1,10 @@ +{ + "dependencies": { }, + "frameworks": { + "dnxcore50": { + "dependencies":{ + "System.Runtime": "4.0.21-*" + } + } + } +} diff --git a/testapp/DthTestProjects/src/FailReleaseProject/Program.cs b/testapp/DthTestProjects/src/FailReleaseProject/Program.cs new file mode 100644 index 000000000..e2e4aa3a1 --- /dev/null +++ b/testapp/DthTestProjects/src/FailReleaseProject/Program.cs @@ -0,0 +1,14 @@ +namespace FailReleaseProject +{ + public class Program + { + public int Main(string[] args) + { +#if RELEASE + // fail the compilation under Release configuration + i +#endif + return 0; + } + } +} diff --git a/testapp/DthTestProjects/src/FailReleaseProject/project.json b/testapp/DthTestProjects/src/FailReleaseProject/project.json new file mode 100644 index 000000000..60da6b421 --- /dev/null +++ b/testapp/DthTestProjects/src/FailReleaseProject/project.json @@ -0,0 +1,10 @@ +{ + "frameworks": { + "dnxcore50": { + "dependencies": { + "System.Runtime": "4.0.21-*" + } + } + }, + "dependencies": { } +} diff --git a/testapp/DthTestProjects/src/IncompatiblePackageSample/project.json b/testapp/DthTestProjects/src/IncompatiblePackageSample/project.json new file mode 100644 index 000000000..dae9b9189 --- /dev/null +++ b/testapp/DthTestProjects/src/IncompatiblePackageSample/project.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "Newtonsoft.Json": "4.5.11" + }, + "frameworks": { + "dnxcore50": { } + } +} diff --git a/testapp/DthTestProjects/src/UnresolvedPackageSample/project.json b/testapp/DthTestProjects/src/UnresolvedPackageSample/project.json new file mode 100644 index 000000000..e9ba88773 --- /dev/null +++ b/testapp/DthTestProjects/src/UnresolvedPackageSample/project.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "NoSuchPackage": "1.0.0" + }, + "frameworks": { + "dnx451": { } + } +} diff --git a/testapp/DthTestProjects/src/UnresolvedProjectSample/project.json b/testapp/DthTestProjects/src/UnresolvedProjectSample/project.json new file mode 100644 index 000000000..1d5589800 --- /dev/null +++ b/testapp/DthTestProjects/src/UnresolvedProjectSample/project.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "EmptyLibrary": "" + }, + "frameworks": { + "dnx451": { } + } +} diff --git a/testapp/DthUpdateSearchPathSample/ext/Newtonsoft.Json/project.json b/testapp/DthUpdateSearchPathSample/ext/Newtonsoft.Json/project.json new file mode 100644 index 000000000..baf23136c --- /dev/null +++ b/testapp/DthUpdateSearchPathSample/ext/Newtonsoft.Json/project.json @@ -0,0 +1,8 @@ +{ + "version": "6.0.8", + "dependencies": { + }, + "frameworks": { + "dnx451": { } + } +} diff --git a/testapp/DthUpdateSearchPathSample/home/global.json b/testapp/DthUpdateSearchPathSample/home/global.json new file mode 100644 index 000000000..c6bd139a9 --- /dev/null +++ b/testapp/DthUpdateSearchPathSample/home/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ + "src", + "../ext" + ] +} diff --git a/testapp/DthUpdateSearchPathSample/home/src/MainProject/project.json b/testapp/DthUpdateSearchPathSample/home/src/MainProject/project.json new file mode 100644 index 000000000..fb6c9c4e9 --- /dev/null +++ b/testapp/DthUpdateSearchPathSample/home/src/MainProject/project.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "Newtonsoft.Json": "6.0.8" + }, + "frameworks": { + "dnx451": { } + } +}