diff --git a/src/dotnet/Program.cs b/src/dotnet/Program.cs index aaa03e890..26bec1a2d 100644 --- a/src/dotnet/Program.cs +++ b/src/dotnet/Program.cs @@ -10,6 +10,7 @@ using System.Text; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Configurer; using Microsoft.DotNet.PlatformAbstractions; +using Microsoft.DotNet.ProjectModel.Server; using Microsoft.DotNet.Tools.Build; using Microsoft.DotNet.Tools.Compiler; using Microsoft.DotNet.Tools.Compiler.Csc; @@ -45,7 +46,8 @@ namespace Microsoft.DotNet.Cli ["run3"] = Run3Command.Run, ["restore3"] = Restore3Command.Run, ["pack3"] = Pack3Command.Run, - ["migrate"] = MigrateCommand.Run + ["migrate"] = MigrateCommand.Run, + ["projectmodel-server"] = ProjectModelServerCommand.Run, }; public static int Main(string[] args) diff --git a/src/dotnet/commands/dotnet-projectmodel-server/ConnectionContext.cs b/src/dotnet/commands/dotnet-projectmodel-server/ConnectionContext.cs new file mode 100644 index 000000000..235db0530 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/ConnectionContext.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.Collections.Generic; +using System.Net.Sockets; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ConnectionContext + { + private readonly string _hostName; + private readonly ProcessingQueue _queue; + private readonly IDictionary _projects; + + public ConnectionContext(Socket acceptedSocket, + string hostName, + ProtocolManager protocolManager, + DesignTimeWorkspace workspaceContext, + IDictionary projects) + { + _hostName = hostName; + _projects = projects; + + _queue = new ProcessingQueue(new NetworkStream(acceptedSocket)); + _queue.OnReceive += message => + { + if (protocolManager.IsProtocolNegotiation(message)) + { + message.Sender = this; + protocolManager.Negotiate(message); + } + else + { + message.Sender = this; + ProjectManager projectManager; + if (!_projects.TryGetValue(message.ContextId, out projectManager)) + { + projectManager = new ProjectManager(message.ContextId, + workspaceContext, + protocolManager); + + _projects[message.ContextId] = projectManager; + } + + projectManager.OnReceive(message); + } + }; + } + + public void QueueStart() + { + _queue.Start(); + } + + public bool Transmit(Message message) + { + message.HostId = _hostName; + return _queue.Send(message); + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Helpers/DependencyTypeChangeFinder.cs b/src/dotnet/commands/dotnet-projectmodel-server/Helpers/DependencyTypeChangeFinder.cs new file mode 100644 index 000000000..7865c3126 --- /dev/null +++ b/src/dotnet/commands/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 previousSearchPaths) + { + var result = new List(); + var project = context.ProjectFile; + var libraries = context.LibraryManager.GetLibraries(); + + var updatedSearchPath = GetUpdatedSearchPaths(previousSearchPaths, 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/dotnet/commands/dotnet-projectmodel-server/Helpers/JTokenExtensions.cs b/src/dotnet/commands/dotnet-projectmodel-server/Helpers/JTokenExtensions.cs new file mode 100644 index 000000000..9edd1d8bf --- /dev/null +++ b/src/dotnet/commands/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/dotnet/commands/dotnet-projectmodel-server/Helpers/LibraryExtensions.cs b/src/dotnet/commands/dotnet-projectmodel-server/Helpers/LibraryExtensions.cs new file mode 100644 index 000000000..2c86daade --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Helpers/LibraryExtensions.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 Microsoft.DotNet.ProjectModel.Graph; + +namespace Microsoft.DotNet.ProjectModel.Server.Helpers +{ + public static class LibraryExtensions + { + public static string GetUniqueName(this LibraryDescription library) + { + var identity = library.Identity; + return identity.Type != LibraryType.ReferenceAssembly ? identity.Name : $"fx/{identity.Name}"; + } + + public static string GetUniqueName(this LibraryRange range) + { + return range.Target != LibraryType.ReferenceAssembly ? range.Name : $"fx/{range.Name}"; + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Helpers/NuGetFrameworkExtensions.cs b/src/dotnet/commands/dotnet-projectmodel-server/Helpers/NuGetFrameworkExtensions.cs new file mode 100644 index 000000000..5bc8c1d53 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Helpers/NuGetFrameworkExtensions.cs @@ -0,0 +1,22 @@ +// 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) + { + return new FrameworkData + { + ShortName = framework.GetShortFolderName(), + FrameworkName = framework.DotNetFrameworkName, + FriendlyName = FrameworkReferenceResolver.Default.GetFriendlyFrameworkName(framework), + RedistListPath = FrameworkReferenceResolver.Default.GetFrameworkRedistListPath(framework) + }; + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Helpers/ProjectExtensions.cs b/src/dotnet/commands/dotnet-projectmodel-server/Helpers/ProjectExtensions.cs new file mode 100644 index 000000000..ef28bbc23 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Helpers/ProjectExtensions.cs @@ -0,0 +1,59 @@ +// 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.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/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectContextSnapshot.cs b/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectContextSnapshot.cs new file mode 100644 index 000000000..1307714d0 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectContextSnapshot.cs @@ -0,0 +1,93 @@ +// 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.Cli.Compiler.Common; +using Microsoft.DotNet.ProjectModel.Files; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.DotNet.ProjectModel.Server.Helpers; +using Microsoft.DotNet.ProjectModel.Server.Models; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ProjectContextSnapshot + { + public string RootDependency { get; set; } + 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 static ProjectContextSnapshot Create(ProjectContext context, string configuration, IEnumerable previousSearchPaths) + { + var snapshot = new ProjectContextSnapshot(); + + var allDependencyDiagnostics = new List(); + allDependencyDiagnostics.AddRange(context.LibraryManager.GetAllDiagnostics()); + allDependencyDiagnostics.AddRange(DependencyTypeChangeFinder.Diagnose(context, previousSearchPaths)); + + var diagnosticsLookup = allDependencyDiagnostics.ToLookup(d => d.Source); + + var allExports = context.CreateExporter(configuration) + .GetAllExports() + .ToDictionary(export => export.Library.Identity.Name); + + var allSourceFiles = new List(GetSourceFiles(context, configuration)); + var allFileReferences = new List(); + var allProjectReferences = new List(); + var allDependencies = new Dictionary(); + + // All exports are returned. When the same library name have a ReferenceAssembly type export and a Package type export + // both will be listed as dependencies. Prefix "fx/" will be added to ReferenceAssembly type dependency. + foreach (var export in allExports.Values) + { + allSourceFiles.AddRange(export.SourceReferences.Select(f => f.ResolvedPath)); + var diagnostics = diagnosticsLookup[export.Library].ToList(); + var description = DependencyDescription.Create(export.Library, diagnostics, allExports); + allDependencies[description.Name] = description; + + var projectReferene = ProjectReferenceDescription.Create(export.Library); + if (projectReferene != null && export.Library.Identity.Name != context.ProjectFile.Name) + { + allProjectReferences.Add(projectReferene); + } + + if (export.Library.Identity.Type != LibraryType.Project) + { + allFileReferences.AddRange(export.CompilationAssemblies.Select(asset => asset.ResolvedPath)); + } + } + + snapshot.RootDependency = context.ProjectFile.Name; + snapshot.TargetFramework = context.TargetFramework; + snapshot.SourceFiles = allSourceFiles.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(path => path).ToList(); + snapshot.CompilerOptions = context.GetLanguageSpecificCompilerOptions(context.TargetFramework, configuration); + snapshot.ProjectReferences = allProjectReferences.OrderBy(reference => reference.Name).ToList(); + snapshot.FileReferences = allFileReferences.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(path => path).ToList(); + snapshot.DependencyDiagnostics = allDependencyDiagnostics; + snapshot.Dependencies = allDependencies; + + return snapshot; + } + + private static IEnumerable GetSourceFiles(ProjectContext context, string configuration) + { + var compilerOptions = context.ProjectFile.GetCompilerOptions(context.TargetFramework, configuration); + + if (compilerOptions.CompileInclude == null) + { + return context.ProjectFile.Files.SourceFiles; + } + + var includeFiles = IncludeFilesResolver.GetIncludeFiles(compilerOptions.CompileInclude, "/", diagnostics: null); + + return includeFiles.Select(f => f.SourcePath); + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectSnapshot.cs b/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectSnapshot.cs new file mode 100644 index 000000000..0b28210bf --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectSnapshot.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; +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 +{ + internal class ProjectSnapshot + { + 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 ProjectContexts { get; } = new Dictionary(); + + public static ProjectSnapshot Create(string projectDirectory, + string configuration, + DesignTimeWorkspace workspaceContext, + IReadOnlyList previousSearchPaths, + bool clearWorkspaceContextCache) + { + var projectContextsCollection = workspaceContext.GetProjectContextCollection(projectDirectory, clearWorkspaceContextCache); + if (!projectContextsCollection.ProjectContexts.Any()) + { + throw new InvalidOperationException($"Unable to find project.json in '{projectDirectory}'"); + } + GlobalSettings globalSettings; + var currentSearchPaths = projectContextsCollection.Project.ResolveSearchPaths(out globalSettings); + + var snapshot = new ProjectSnapshot(); + snapshot.Project = projectContextsCollection.Project; + snapshot.ProjectDiagnostics = new List(projectContextsCollection.ProjectDiagnostics); + snapshot.ProjectSearchPaths = currentSearchPaths.ToList(); + snapshot.GlobalJsonPath = globalSettings?.FilePath; + + foreach (var projectContext in projectContextsCollection.FrameworkOnlyContexts) + { + snapshot.ProjectContexts[projectContext.TargetFramework] = + ProjectContextSnapshot.Create(projectContext, configuration, previousSearchPaths); + } + + return snapshot; + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/MessageTypes.cs b/src/dotnet/commands/dotnet-projectmodel-server/MessageTypes.cs new file mode 100644 index 000000000..df9419502 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/MessageTypes.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 +{ + public class MessageTypes + { + // Incoming + 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); + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Messengers/CompilerOptionsMessenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/CompilerOptionsMessenger.cs new file mode 100644 index 000000000..57c46426b --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/CompilerOptionsMessenger.cs @@ -0,0 +1,42 @@ +// 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; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class CompilerOptionsMessenger : Messenger + { + public CompilerOptionsMessenger(Action transmit) + : base(MessageTypes.CompilerOptions, transmit) + { } + + protected override bool CheckDifference(ProjectContextSnapshot local, ProjectContextSnapshot remote) + { + return remote.CompilerOptions != null && + Equals(local.CompilerOptions, remote.CompilerOptions); + } + + protected override void SendPayload(ProjectContextSnapshot local, Action send) + { + send(new CompilationOptionsMessage + { + Framework = local.TargetFramework.ToPayload(), + Options = local.CompilerOptions + }); + } + + protected override void SetValue(ProjectContextSnapshot local, ProjectContextSnapshot remote) + { + remote.CompilerOptions = local.CompilerOptions; + } + + private class CompilationOptionsMessage + { + public FrameworkData Framework { get; set; } + + public CommonCompilerOptions Options { get; set; } + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Messengers/DependenciesMessenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/DependenciesMessenger.cs new file mode 100644 index 000000000..e89fcabc4 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/DependenciesMessenger.cs @@ -0,0 +1,47 @@ +// 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.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class DependenciesMessenger : Messenger + { + public DependenciesMessenger(Action transmit) + : base(MessageTypes.Dependencies, transmit) + { } + + protected override bool CheckDifference(ProjectContextSnapshot local, ProjectContextSnapshot remote) + { + return remote.Dependencies != null && + string.Equals(local.RootDependency, remote.RootDependency) && + Equals(local.TargetFramework, remote.TargetFramework) && + Enumerable.SequenceEqual(local.Dependencies, remote.Dependencies); + } + + protected override void SendPayload(ProjectContextSnapshot local, Action send) + { + send(new DependenciesMessage + { + Framework = local.TargetFramework.ToPayload(), + RootDependency = local.RootDependency, + Dependencies = local.Dependencies + }); + } + + protected override void SetValue(ProjectContextSnapshot local, ProjectContextSnapshot 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/dotnet/commands/dotnet-projectmodel-server/Messengers/DependencyDiagnosticsMessenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/DependencyDiagnosticsMessenger.cs new file mode 100644 index 000000000..fa95b756b --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/DependencyDiagnosticsMessenger.cs @@ -0,0 +1,34 @@ +// 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 DependencyDiagnosticsMessenger : Messenger + { + public DependencyDiagnosticsMessenger(Action transmit) + : base(MessageTypes.DependencyDiagnostics, transmit) + { } + + protected override bool CheckDifference(ProjectContextSnapshot local, ProjectContextSnapshot remote) + { + return remote.DependencyDiagnostics != null && + Enumerable.SequenceEqual(local.DependencyDiagnostics, remote.DependencyDiagnostics); + } + + protected override void SendPayload(ProjectContextSnapshot local, Action send) + { + send(new DiagnosticsListMessage( + local.DependencyDiagnostics, + local.TargetFramework?.ToPayload())); + } + + protected override void SetValue(ProjectContextSnapshot local, ProjectContextSnapshot remote) + { + remote.DependencyDiagnostics = local.DependencyDiagnostics; + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Messengers/GlobalErrorMessenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/GlobalErrorMessenger.cs new file mode 100644 index 000000000..854465ef8 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/GlobalErrorMessenger.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.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class GlobalErrorMessenger : Messenger + { + public GlobalErrorMessenger(Action transmit) + : base(MessageTypes.Error, transmit) + { } + + protected override bool CheckDifference(ProjectSnapshot local, ProjectSnapshot remote) + { + return remote != null && Equals(local.GlobalErrorMessage, remote.GlobalErrorMessage); + } + + protected override void SendPayload(ProjectSnapshot local, Action send) + { + if (local.GlobalErrorMessage != null) + { + send(local.GlobalErrorMessage); + } + else + { + send(new ErrorMessage + { + Message = null, + Path = null, + Line = -1, + Column = -1 + }); + } + } + + protected override void SetValue(ProjectSnapshot local, ProjectSnapshot remote) + { + remote.GlobalErrorMessage = local.GlobalErrorMessage; + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Messengers/Messenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/Messenger.cs new file mode 100644 index 000000000..5805e9425 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/Messenger.cs @@ -0,0 +1,34 @@ +// 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 abstract class Messenger where T : class + { + protected readonly Action _transmit; + + public Messenger(string messageType, Action transmit) + { + _transmit = transmit; + + MessageType = messageType; + } + + public string MessageType { get; } + + public void UpdateRemote(T local, T remote) + { + if (!CheckDifference(local, remote)) + { + SendPayload(local, payload => _transmit(MessageType, payload)); + SetValue(local, remote); + } + } + + protected abstract void SetValue(T local, T remote); + protected abstract void SendPayload(T local, Action send); + protected abstract bool CheckDifference(T local, T remote); + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Messengers/ProjectDiagnosticsMessenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/ProjectDiagnosticsMessenger.cs new file mode 100644 index 000000000..4ef95660d --- /dev/null +++ b/src/dotnet/commands/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(ProjectSnapshot local, ProjectSnapshot remote) + { + return remote.ProjectDiagnostics != null && + Enumerable.SequenceEqual(local.ProjectDiagnostics, remote.ProjectDiagnostics); + } + + protected override void SendPayload(ProjectSnapshot local, Action send) + { + send(new DiagnosticsListMessage(local.ProjectDiagnostics)); + } + + protected override void SetValue(ProjectSnapshot local, ProjectSnapshot remote) + { + remote.ProjectDiagnostics = local.ProjectDiagnostics; + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Messengers/ProjectInformationMessenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/ProjectInformationMessenger.cs new file mode 100644 index 000000000..096f42747 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/ProjectInformationMessenger.cs @@ -0,0 +1,68 @@ +// 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.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class ProjectInformationMessenger : Messenger + { + public ProjectInformationMessenger(Action transmit) + : base(MessageTypes.ProjectInformation, transmit) + { } + + protected override bool CheckDifference(ProjectSnapshot local, ProjectSnapshot 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 void SendPayload(ProjectSnapshot local, Action send) + { + send(new ProjectInformationMessage(local.Project, local.GlobalJsonPath, local.ProjectSearchPaths)); + } + + protected override void SetValue(ProjectSnapshot local, ProjectSnapshot remote) + { + remote.Project = local.Project; + remote.GlobalJsonPath = local.GlobalJsonPath; + remote.ProjectSearchPaths = local.ProjectSearchPaths; + } + + private class ProjectInformationMessage + { + public ProjectInformationMessage(Project project, + string gloablJsonPath, + IReadOnlyList projectSearchPaths) + { + Name = project.Name; + Frameworks = project.GetTargetFrameworks().Select(f => f.FrameworkName.ToPayload()).ToList(); + Configurations = project.GetConfigurations().ToList(); + Commands = project.Commands; + ProjectSearchPaths = projectSearchPaths; + 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/dotnet/commands/dotnet-projectmodel-server/Messengers/ReferencesMessenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/ReferencesMessenger.cs new file mode 100644 index 000000000..04a06af0b --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/ReferencesMessenger.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.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class ReferencesMessenger : Messenger + { + public ReferencesMessenger(Action transmit) + : base(MessageTypes.References, transmit) + { } + + protected override bool CheckDifference(ProjectContextSnapshot local, ProjectContextSnapshot remote) + { + return remote.FileReferences != null && + remote.ProjectReferences != null && + Enumerable.SequenceEqual(local.FileReferences, remote.FileReferences) && + Enumerable.SequenceEqual(local.ProjectReferences, remote.ProjectReferences); + } + + protected override void SendPayload(ProjectContextSnapshot local, Action send) + { + send(new ReferencesMessage + { + Framework = local.TargetFramework.ToPayload(), + ProjectReferences = local.ProjectReferences, + FileReferences = local.FileReferences + }); + } + + protected override void SetValue(ProjectContextSnapshot local, ProjectContextSnapshot 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/dotnet/commands/dotnet-projectmodel-server/Messengers/SourcesMessenger.cs b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/SourcesMessenger.cs new file mode 100644 index 000000000..66f7767df --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Messengers/SourcesMessenger.cs @@ -0,0 +1,45 @@ +// 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.Models; + +namespace Microsoft.DotNet.ProjectModel.Server.Messengers +{ + internal class SourcesMessenger : Messenger + { + public SourcesMessenger(Action transmit) + : base(MessageTypes.Sources, transmit) + { } + + protected override bool CheckDifference(ProjectContextSnapshot local, ProjectContextSnapshot remote) + { + return remote.SourceFiles != null && + Enumerable.SequenceEqual(local.SourceFiles, remote.SourceFiles); + } + + protected override void SendPayload(ProjectContextSnapshot local, Action send) + { + send(new SourcesMessage + { + Framework = local.TargetFramework.ToPayload(), + Files = local.SourceFiles, + GeneratedFiles = new Dictionary() + }); + } + + protected override void SetValue(ProjectContextSnapshot local, ProjectContextSnapshot remote) + { + remote.SourceFiles = local.SourceFiles; + } + + private class SourcesMessage + { + public FrameworkData Framework { get; set; } + public IReadOnlyList Files { get; set; } + public IDictionary GeneratedFiles { get; set; } + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Models/DependencyDescription.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/DependencyDescription.cs new file mode 100644 index 000000000..32c3854e5 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Models/DependencyDescription.cs @@ -0,0 +1,94 @@ +// 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.Compilation; +using Microsoft.DotNet.ProjectModel.Graph; +using NuGet.Versioning; + +namespace Microsoft.DotNet.ProjectModel.Server.Models +{ + public class DependencyDescription + { + private DependencyDescription() { } + + 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(); + } + + public static DependencyDescription Create(LibraryDescription library, + List diagnostics, + IDictionary exportsLookup) + { + var result = new DependencyDescription + { + Name = library.Identity.Name, + DisplayName = library.Identity.Name, + Version = (library.Identity.Version ?? new NuGetVersion("1.0.0")).ToNormalizedString(), + Type = library.Identity.Type.Value, + Resolved = library.Resolved, + Path = library.Path, + Dependencies = library.Dependencies.Select(dependency => GetDependencyItem(dependency, exportsLookup)), + 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)) + }; + + var msbuildLibrary = library as MSBuildProjectDescription; + if (msbuildLibrary != null) + { + result.Path = msbuildLibrary.MSBuildProjectPath; + } + + return result; + } + + private static DependencyItem GetDependencyItem(LibraryRange dependency, + IDictionary exportsLookup) + { + return new DependencyItem + { + Name = dependency.Name, + Version = exportsLookup[dependency.Name].Library.Identity.Version?.ToNormalizedString() + }; + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Models/DependencyItem.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/DependencyItem.cs new file mode 100644 index 000000000..1f014daff --- /dev/null +++ b/src/dotnet/commands/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/dotnet/commands/dotnet-projectmodel-server/Models/DiagnosticMessageGroup.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/DiagnosticMessageGroup.cs new file mode 100644 index 000000000..c76adf86c --- /dev/null +++ b/src/dotnet/commands/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/dotnet/commands/dotnet-projectmodel-server/Models/DiagnosticMessageView.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/DiagnosticMessageView.cs new file mode 100644 index 000000000..8285d48ba --- /dev/null +++ b/src/dotnet/commands/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/dotnet/commands/dotnet-projectmodel-server/Models/DiagnosticsListMessage.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/DiagnosticsListMessage.cs new file mode 100644 index 000000000..dc66cd12d --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Models/DiagnosticsListMessage.cs @@ -0,0 +1,69 @@ +// 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; + +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/dotnet/commands/dotnet-projectmodel-server/Models/ErrorMessage.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/ErrorMessage.cs new file mode 100644 index 000000000..8623afae3 --- /dev/null +++ b/src/dotnet/commands/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/dotnet/commands/dotnet-projectmodel-server/Models/FrameworkData.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/FrameworkData.cs new file mode 100644 index 000000000..258c1cb08 --- /dev/null +++ b/src/dotnet/commands/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/dotnet/commands/dotnet-projectmodel-server/Models/Message.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/Message.cs new file mode 100644 index 000000000..0c0bfe8f3 --- /dev/null +++ b/src/dotnet/commands/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/dotnet/commands/dotnet-projectmodel-server/Models/ProjectReferenceDescription.cs b/src/dotnet/commands/dotnet-projectmodel-server/Models/ProjectReferenceDescription.cs new file mode 100644 index 000000000..224837488 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Models/ProjectReferenceDescription.cs @@ -0,0 +1,57 @@ +// 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 FrameworkData Framework { get; set; } + public string Name { get; set; } + public string Path { 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); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Create a ProjectReferenceDescription from given LibraryDescription. If the library doesn't + /// represent a project reference returns null. + /// + public static ProjectReferenceDescription Create(LibraryDescription library) + { + if (library is ProjectDescription) + { + return new ProjectReferenceDescription + { + Framework = library.Framework.ToPayload(), + Name = library.Identity.Name, + Path = library.Path + }; + } + else if (library is MSBuildProjectDescription) + { + return new ProjectReferenceDescription + { + Framework = library.Framework.ToPayload(), + Name = library.Identity.Name, + Path = ((MSBuildProjectDescription)library).MSBuildProjectPath, + }; + } + else + { + return null; + } + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/ProcessingQueue.cs b/src/dotnet/commands/dotnet-projectmodel-server/ProcessingQueue.cs new file mode 100644 index 000000000..d3ceb13e9 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/ProcessingQueue.cs @@ -0,0 +1,88 @@ +// 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.Cli.Utils; +using Microsoft.DotNet.ProjectModel.Server.Models; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ProcessingQueue + { + private readonly BinaryReader _reader; + private readonly BinaryWriter _writer; + + public ProcessingQueue(Stream stream) + { + _reader = new BinaryReader(stream); + _writer = new BinaryWriter(stream); + } + + public event Action OnReceive; + + public void Start() + { + Reporter.Output.WriteLine("Start"); + new Thread(ReceiveMessages).Start(); + } + + public bool Send(Action writeAction) + { + lock (_writer) + { + try + { + writeAction(_writer); + return true; + } + catch (IOException ex) + { + // swallow + Reporter.Output.WriteLine($"Ignore {nameof(IOException)} during sending message: \"{ex.Message}\"."); + } + catch (Exception ex) + { + Reporter.Output.WriteLine($"Unexpected exception {ex.GetType().Name} during sending message: \"{ex.Message}\"."); + throw; + } + } + + return false; + } + + public bool Send(Message message) + { + return Send(_writer => + { + Reporter.Output.WriteLine($"OnSend ({message})"); + _writer.Write(JsonConvert.SerializeObject(message)); + }); + } + + private void ReceiveMessages() + { + try + { + while (true) + { + var content = _reader.ReadString(); + var message = JsonConvert.DeserializeObject(content); + + Reporter.Output.WriteLine($"OnReceive ({message})"); + OnReceive(message); + } + } + catch (IOException ex) + { + Reporter.Output.WriteLine($"Ignore {nameof(IOException)} during receiving messages: \"{ex}\"."); + } + catch (Exception ex) + { + Reporter.Error.WriteLine($"Unexpected exception {ex.GetType().Name} during receiving messages: \"{ex}\"."); + } + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Program.cs b/src/dotnet/commands/dotnet-projectmodel-server/Program.cs new file mode 100644 index 000000000..3211ed15d --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/Program.cs @@ -0,0 +1,161 @@ +// 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.Net; +using System.Net.Sockets; +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + public class ProjectModelServerCommand + { + private readonly Dictionary _projects; + private readonly DesignTimeWorkspace _workspaceContext; + private readonly ProtocolManager _protocolManager; + private readonly string _hostName; + private readonly int _port; + private Socket _listenSocket; + + public ProjectModelServerCommand(int port, string hostName) + { + _port = port; + _hostName = hostName; + _protocolManager = new ProtocolManager(maxVersion: 4); + _workspaceContext = new DesignTimeWorkspace(ProjectReaderSettings.ReadFromEnvironment()); + _projects = new Dictionary(); + } + + public static int Run(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("--host-pid", "The process id of the host", CommandOptionType.SingleValue); + var hostname = app.Option("--host-name", "The name of the host", CommandOptionType.SingleValue); + var port = app.Option("--port", "The TCP port used for communication", CommandOptionType.SingleValue); + + app.OnExecute(() => + { + try + { + if (!MonitorHostProcess(hostpid)) + { + return 1; + } + + var intPort = CheckPort(port); + if (intPort == -1) + { + return 1; + } + + if (!hostname.HasValue()) + { + Reporter.Error.WriteLine($"Option \"{hostname.LongName}\" is missing."); + return 1; + } + + var program = new ProjectModelServerCommand(intPort, hostname.Value()); + program.OpenChannel(); + } + catch (Exception ex) + { + Reporter.Error.WriteLine($"Unhandled exception in server main: {ex}"); + throw; + } + + return 0; + }); + + return app.Execute(args); + } + + public void OpenChannel() + { + _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _listenSocket.Bind(new IPEndPoint(IPAddress.Loopback, _port)); + _listenSocket.Listen(10); + + Reporter.Output.WriteLine($"Process ID {Process.GetCurrentProcess().Id}"); + Reporter.Output.WriteLine($"Listening on port {_port}"); + + while (true) + { + var acceptSocket = _listenSocket.Accept(); + Reporter.Output.WriteLine($"Client accepted {acceptSocket.LocalEndPoint}"); + + var connection = new ConnectionContext(acceptSocket, + _hostName, + _protocolManager, + _workspaceContext, + _projects); + + connection.QueueStart(); + } + } + + public void Shutdown() + { + if (_listenSocket.Connected) + { + _listenSocket.Shutdown(SocketShutdown.Both); + } + } + + private static int CheckPort(CommandOption port) + { + if (!port.HasValue()) + { + Reporter.Error.WriteLine($"Option \"{port.LongName}\" is missing."); + } + + int result; + if (int.TryParse(port.Value(), out result)) + { + return result; + } + else + { + Reporter.Error.WriteLine($"Option \"{port.LongName}\" is not a valid Int32 value."); + return -1; + } + } + + private static bool MonitorHostProcess(CommandOption host) + { + if (!host.HasValue()) + { + Console.Error.WriteLine($"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(); + }; + + Reporter.Output.WriteLine($"Server will exit when process {hostPID} exits."); + return true; + } + else + { + Reporter.Error.WriteLine($"Option \"{host.LongName}\" is not a valid Int32 value."); + return false; + } + } + } +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/ProjectManager.cs b/src/dotnet/commands/dotnet-projectmodel-server/ProjectManager.cs new file mode 100644 index 000000000..47f4a0390 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/ProjectManager.cs @@ -0,0 +1,327 @@ +// 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.Cli.Utils; +using Microsoft.DotNet.ProjectModel.Server.Helpers; +using Microsoft.DotNet.ProjectModel.Server.Messengers; +using Microsoft.DotNet.ProjectModel.Server.Models; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ProjectManager + { + private readonly object _processingLock = new object(); + private readonly Queue _inbox = new Queue(); + private readonly ProtocolManager _protocolManager; + + 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 ProjectSnapshot _local = new ProjectSnapshot(); + private ProjectSnapshot _remote = new ProjectSnapshot(); + + private readonly DesignTimeWorkspace _workspaceContext; + private int? _contextProtocolVersion; + + private readonly List> _messengers; + + private ProjectDiagnosticsMessenger _projectDiagnosticsMessenger; + private GlobalErrorMessenger _globalErrorMessenger; + private ProjectInformationMessenger _projectInforamtionMessenger; + + public ProjectManager( + int contextId, + DesignTimeWorkspace workspaceContext, + ProtocolManager protocolManager) + { + Id = contextId; + _workspaceContext = workspaceContext; + _protocolManager = protocolManager; + + _messengers = new List> + { + new ReferencesMessenger(Transmit), + new DependenciesMessenger(Transmit), + new DependencyDiagnosticsMessenger(Transmit), + new CompilerOptionsMessenger(Transmit), + new SourcesMessenger(Transmit) + }; + + _projectDiagnosticsMessenger = new ProjectDiagnosticsMessenger(Transmit); + _globalErrorMessenger = new GlobalErrorMessenger(Transmit); + _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 => ((ProjectManager)state).ProcessLoop(), this); + } + + private void Transmit(string messageType, object payload) + { + var message = Message.FromPayload(messageType, Id, payload); + _initializedContext.Transmit(message); + } + + private void ProcessLoop() + { + if (!Monitor.TryEnter(_processingLock)) + { + return; + } + + try + { + lock (_inbox) + { + if (!_inbox.Any()) + { + return; + } + } + + DoProcessLoop(); + } + catch (Exception ex) + { + Reporter.Error.WriteLine($"A unexpected exception 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; + } + finally + { + Monitor.Exit(_processingLock); + } + } + + private void DoProcessLoop() + { + while (true) + { + DrainInbox(); + + UpdateProject(); + SendOutgingMessages(); + + lock (_inbox) + { + if (_inbox.Count == 0) + { + return; + } + } + } + } + + private void DrainInbox() + { + Reporter.Output.WriteLine("Begin draining inbox."); + + while (ProcessMessage()) { } + + Reporter.Output.WriteLine("Finish draining inbox."); + } + + private bool ProcessMessage() + { + Message message; + + lock (_inbox) + { + if (!_inbox.Any()) + { + return false; + } + + message = _inbox.Dequeue(); + Debug.Assert(message != null); + } + + Reporter.Output.WriteLine($"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: + // In the case of RefreshDependencies request, the cache will not be reset in any case. The value + // is set so as to trigger refresh action in later loop. + _refreshDependencies.Value = false; + break; + case MessageTypes.RestoreComplete: + // In the case of RestoreComplete request, the value of the 'Reset' property in payload will determine + // if the cache should be reset. If the property doesn't exist, cache will be reset. + _refreshDependencies.Value = message.Payload.HasValues ? message.Payload.Value("Reset") : true; + break; + case MessageTypes.FilesChanged: + _filesChanged.Value = 0; + break; + } + + return true; + } + + private void Initialize(Message message) + { + if (_initializedContext != null) + { + Reporter.Output.WriteLine($"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); + Reporter.Output.WriteLine($"Set context protocol version to {_contextProtocolVersion.Value}"); + } + } + + private bool UpdateProject() + { + ProjectSnapshot newSnapshot = null; + + if (_appPath.WasAssigned || _configure.WasAssigned || _filesChanged.WasAssigned || _refreshDependencies.WasAssigned) + { + _appPath.ClearAssigned(); + _configure.ClearAssigned(); + _filesChanged.ClearAssigned(); + + bool resetCache = _refreshDependencies.WasAssigned ? _refreshDependencies.Value : false; + _refreshDependencies.ClearAssigned(); + + newSnapshot = ProjectSnapshot.Create(_appPath.Value, + _configure.Value, + _workspaceContext, + _remote.ProjectSearchPaths, + clearWorkspaceContextCache: resetCache); + } + + if (newSnapshot == null) + { + return false; + } + + _local = newSnapshot; + + return true; + } + + private void SendOutgingMessages() + { + _projectInforamtionMessenger.UpdateRemote(_local, _remote); + _projectDiagnosticsMessenger.UpdateRemote(_local, _remote); + + var unprocessedFrameworks = new HashSet(_remote.ProjectContexts.Keys); + foreach (var pair in _local.ProjectContexts) + { + ProjectContextSnapshot localProjectSnapshot = pair.Value; + ProjectContextSnapshot remoteProjectSnapshot; + + if (!_remote.ProjectContexts.TryGetValue(pair.Key, out remoteProjectSnapshot)) + { + remoteProjectSnapshot = new ProjectContextSnapshot(); + _remote.ProjectContexts[pair.Key] = remoteProjectSnapshot; + } + + 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.ProjectContexts.Remove(framework); + } + + _globalErrorMessenger.UpdateRemote(_local, _remote); + } + + 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/dotnet/commands/dotnet-projectmodel-server/ProtocolManager.cs b/src/dotnet/commands/dotnet-projectmodel-server/ProtocolManager.cs new file mode 100644 index 000000000..dd7de15c9 --- /dev/null +++ b/src/dotnet/commands/dotnet-projectmodel-server/ProtocolManager.cs @@ -0,0 +1,107 @@ +// 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.Cli.Utils; +using Microsoft.DotNet.ProjectModel.Server.Models; + +namespace Microsoft.DotNet.ProjectModel.Server +{ + internal class ProtocolManager + { + /// + /// Environment variable for overriding protocol. + /// + public const string EnvDthProtocol = "DTH_PROTOCOL"; + + public ProtocolManager(int maxVersion) + { + MaxVersion = maxVersion; + + // 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; + } + + Reporter.Output.WriteLine("Initializing the protocol negotiation."); + + if (EnvironmentOverridden) + { + Reporter.Output.WriteLine($"DTH protocol negotiation is override by environment variable {EnvDthProtocol} and set to {CurrentVersion}."); + return; + } + + var tokenValue = message.Payload?["Version"]; + if (tokenValue == null) + { + Reporter.Output.WriteLine("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. + Reporter.Output.WriteLine("Protocol negotiation failed. Protocol version 0 is invalid."); + return; + } + + CurrentVersion = Math.Min(preferredVersion, MaxVersion); + Reporter.Output.WriteLine($"Protocol negotiation successed. Use protocol {CurrentVersion}"); + + if (message.Sender != null) + { + Reporter.Output.WriteLine("Respond to protocol negotiation."); + message.Sender.Transmit(Message.FromPayload( + MessageTypes.ProtocolVersion, + 0, + new { Version = CurrentVersion })); + } + else + { + Reporter.Output.WriteLine($"{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; + } + } +}