diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependencyNugetUpdateBlock.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependencyNugetUpdateBlock.cs new file mode 100644 index 00000000000..058a229cce9 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependencyNugetUpdateBlock.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Text.RegularExpressions; +using System.Threading.Tasks.Dataflow; +using System.Timers; +using Microsoft.VisualStudio.Collections; +using Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies.Models; + +namespace Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies; + +[Export(typeof(IDependencyNugetUpdateBlock))] +internal class DependencyNugetUpdateBlock: ProjectValueDataSourceBase>, IDependencyNugetUpdateBlock +{ + private int _sourceVersion; + + private IBroadcastBlock>> _broadcastBlock = null!; + + private IReceivableSourceBlock>> _publicBlock = null!; + + private Dictionary? _lastPublishedValue; + + public override NamedIdentity DataSourceKey { get; } = new(nameof(DependencyNugetUpdateBlock)); + + public override IComparable DataSourceVersion => _sourceVersion; + + [ImportingConstructor] + public DependencyNugetUpdateBlock(UnconfiguredProject unconfiguredProject) + : base(unconfiguredProject.Services, synchronousDisposal: false, registerDataSource: false) + { + } + + public override IReceivableSourceBlock>> SourceBlock + { + get + { + EnsureInitialized(); + return _publicBlock; + } + } + + protected override void Initialize() + { +#pragma warning disable RS0030 + base.Initialize(); +#pragma warning restore RS0030 + + _broadcastBlock = DataflowBlockSlim.CreateBroadcastBlock>>(nameFormat: $"{nameof(DependencyNugetUpdateBlock)} {1}"); + + _publicBlock = _broadcastBlock.SafePublicize(); + + PostNewValue(GetNewValue()); // TODO currently blocks receiving dependency model to make initial request + + var timer = new System.Timers.Timer(); + timer.Elapsed += OnRefreshDependencyStatus; + timer.Interval = TimeSpan.FromMinutes(15).TotalMilliseconds; + timer.Start(); + } + + private void OnRefreshDependencyStatus(object sender, ElapsedEventArgs elapsedEventArgs) + { + PostNewValue(GetNewValue()); + } + + private string RunCommandSynchronouslyAndReceiveOutput(string command) + { + var process = new System.Diagnostics.Process(); + var startInfo = new System.Diagnostics.ProcessStartInfo + { + WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden, + FileName = "cmd.exe", + Arguments = $"/C {command}", + RedirectStandardOutput = true, + UseShellExecute = false + }; + + process.StartInfo = startInfo; + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + return output; + } + + private Dictionary GetNewValue() + { + Dictionary packageDiagnosticLevels = new(); + + string dotnetListVulnerableCommandOutput = RunCommandSynchronouslyAndReceiveOutput("dotnet list package --vulnerable"); + string dotnetListOutdatedCommandOutput = RunCommandSynchronouslyAndReceiveOutput("dotnet list package --outdated"); + string dotnetListDeprecatedCommandOutput = RunCommandSynchronouslyAndReceiveOutput("dotnet list package --deprecated"); + + foreach (Match match in Regex.Matches(dotnetListVulnerableCommandOutput, "> ([^\\s]+)\\s+")) + { + AddPackageIfLevelHasPriority(match.Groups[1].Value, DiagnosticLevel.Vulnerability); + } + + foreach (Match match in Regex.Matches(dotnetListOutdatedCommandOutput, "> ([^\\s]+)\\s+")) + { + AddPackageIfLevelHasPriority(match.Groups[1].Value, DiagnosticLevel.UpgradeAvailable); + } + + foreach (Match match in Regex.Matches(dotnetListDeprecatedCommandOutput, "> ([^\\s]+)\\s+")) + { + AddPackageIfLevelHasPriority(match.Groups[1].Value, DiagnosticLevel.Deprecation); + } + + void AddPackageIfLevelHasPriority(string package, DiagnosticLevel level) + { + if (!packageDiagnosticLevels.TryGetValue(package, out DiagnosticLevel existingValue) || existingValue < level) + { + packageDiagnosticLevels[package] = level; + } + } + + return packageDiagnosticLevels; + } + + private void PostNewValue(Dictionary newValue) + { + // Add thread safety as needed. Make sure to never regress the data source version published + if (!DictionaryEqualityComparer.Instance.Equals(newValue, _lastPublishedValue)) // only publish if you have to + { + _lastPublishedValue = newValue; + _broadcastBlock.Post( + new ProjectVersionedValue>( + newValue, + ImmutableDictionary.Create().Add( + DataSourceKey, + _sourceVersion++))); + } + } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Models/DependenciesViewModelFactory.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Models/DependenciesViewModelFactory.cs index 143009968d6..0c8acf044bc 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Models/DependenciesViewModelFactory.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Models/DependenciesViewModelFactory.cs @@ -50,10 +50,16 @@ public IDependencyViewModel CreateTargetViewModel(TargetFramework targetFramewor public ImageMoniker GetDependenciesRootIcon(DiagnosticLevel maximumDiagnosticLevel) { + // TODO update upgradeavailable/deprecation/vulnerability icons return maximumDiagnosticLevel switch { DiagnosticLevel.None => KnownMonikers.ReferenceGroup, - _ => KnownMonikers.ReferenceGroupWarning + DiagnosticLevel.UpgradeAvailable => KnownMonikers.OfficeWord2013, + DiagnosticLevel.Warning => KnownMonikers.ReferenceGroupWarning, + DiagnosticLevel.Deprecation => KnownMonikers.OfficeSharePoint2013, + DiagnosticLevel.Error => KnownMonikers.ReferenceGroupError, + DiagnosticLevel.Vulnerability => KnownMonikers.OfficeExcel2013, + _ => throw new ArgumentOutOfRangeException() }; } } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Models/DependencyModel.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Models/DependencyModel.cs index a4b043cd8cb..1050f350b26 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Models/DependencyModel.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Models/DependencyModel.cs @@ -12,8 +12,11 @@ internal enum DiagnosticLevel // These states are in precedence order, where later states override earlier ones. None = 0, - Warning = 1, - Error = 2, + UpgradeAvailable = 1, + Warning = 2, + Deprecation = 3, + Error = 4, + Vulnerability = 5 } /// @@ -63,8 +66,11 @@ protected DependencyModel( { diagnosticLevel = levelString switch { - "Warning" => DiagnosticLevel.Warning, - "Error" => DiagnosticLevel.Error, + nameof(DiagnosticLevel.Warning) => DiagnosticLevel.Warning, + nameof(DiagnosticLevel.Error) => DiagnosticLevel.Error, + nameof(DiagnosticLevel.UpgradeAvailable) => DiagnosticLevel.UpgradeAvailable, + nameof(DiagnosticLevel.Deprecation) => DiagnosticLevel.Deprecation, + nameof(DiagnosticLevel.Vulnerability) => DiagnosticLevel.Vulnerability, _ => DiagnosticLevel.None }; } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Snapshot/Dependency.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Snapshot/Dependency.cs index 82d2c61f577..84087d9dc47 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Snapshot/Dependency.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Snapshot/Dependency.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. using Microsoft.VisualStudio.Buffers.PooledObjects; +using Microsoft.VisualStudio.Imaging; using Microsoft.VisualStudio.Imaging.Interop; using Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies.Models; using Microsoft.VisualStudio.ProjectSystem.VS.Tree.Dependencies; @@ -122,8 +123,70 @@ private Dependency( public string? FilePath { get; } - public ImageMoniker Icon => DiagnosticLevel == DiagnosticLevel.None ? Implicit ? IconSet.ImplicitIcon : IconSet.Icon : IconSet.UnresolvedIcon; - public ImageMoniker ExpandedIcon => DiagnosticLevel == DiagnosticLevel.None ? Implicit ? IconSet.ImplicitExpandedIcon : IconSet.ExpandedIcon : IconSet.UnresolvedExpandedIcon; + public ImageMoniker Icon + { + get + { + if (DiagnosticLevel == DiagnosticLevel.None) + { + if (Implicit) + { + return IconSet.ImplicitIcon; + } + + return IconSet.Icon; + } + + switch (DiagnosticLevel) + { + case DiagnosticLevel.UpgradeAvailable: + return KnownMonikers.OfficeWord2013; + case DiagnosticLevel.Warning: + return IconSet.UnresolvedIcon; + case DiagnosticLevel.Deprecation: + return KnownMonikers.OfficeSharePoint2013; + case DiagnosticLevel.Error: + return IconSet.UnresolvedIcon; + case DiagnosticLevel.Vulnerability: + return KnownMonikers.OfficeExcel2013; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + public ImageMoniker ExpandedIcon + { + get + { + if (DiagnosticLevel == DiagnosticLevel.None) + { + if (Implicit) + { + return IconSet.ImplicitExpandedIcon; + } + + return IconSet.ExpandedIcon; + } + + // TODO update upgradeavailable/deprecation/vulnerability icons + switch (DiagnosticLevel) + { + case DiagnosticLevel.UpgradeAvailable: + return KnownMonikers.OfficeWord2013; + case DiagnosticLevel.Warning: + return IconSet.UnresolvedIcon; + case DiagnosticLevel.Deprecation: + return KnownMonikers.OfficeSharePoint2013; + case DiagnosticLevel.Error: + return IconSet.UnresolvedExpandedIcon; + case DiagnosticLevel.Vulnerability: + return KnownMonikers.OfficeExcel2013; + default: + throw new ArgumentOutOfRangeException(); + } + } + } #endregion diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/RuleHandlers/PackageRuleHandler.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/RuleHandlers/PackageRuleHandler.cs index e4520ce869c..4bbc97070b6 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/RuleHandlers/PackageRuleHandler.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/Subscriptions/RuleHandlers/PackageRuleHandler.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks.Dataflow; using Microsoft.VisualStudio.Buffers.PooledObjects; using Microsoft.VisualStudio.Imaging; using Microsoft.VisualStudio.ProjectSystem.Properties; @@ -30,12 +31,14 @@ internal sealed class PackageRuleHandler : DependenciesRuleHandlerBase DependencyTreeFlags.PackageDependencyGroup); private readonly ITargetFrameworkProvider _targetFrameworkProvider; + private readonly IDependencyNugetUpdateBlock _dependencyNugetUpdateBlock; [ImportingConstructor] - public PackageRuleHandler(ITargetFrameworkProvider targetFrameworkProvider) + public PackageRuleHandler(ITargetFrameworkProvider targetFrameworkProvider, IDependencyNugetUpdateBlock dependencyNugetUpdateBlock) : base(PackageReference.SchemaName, ResolvedPackageReference.SchemaName) { _targetFrameworkProvider = targetFrameworkProvider; + _dependencyNugetUpdateBlock = dependencyNugetUpdateBlock; } public override string ProviderType => ProviderTypeString; @@ -193,6 +196,17 @@ private bool TryCreatePackageDependencyModel( return false; } + DiagnosticLevel diagnosticLevel = properties.TryGetValue(ProjectItemMetadata.DiagnosticLevel, out string levelString) + ? Enum.TryParse(levelString, out DiagnosticLevel level) ? level : DiagnosticLevel.None + : DiagnosticLevel.None; + Dictionary packageDiagnosticLevels = _dependencyNugetUpdateBlock.SourceBlock.Receive().Value; + + if (packageDiagnosticLevels.TryGetValue(originalItemSpec, out DiagnosticLevel foundLevel) && foundLevel > diagnosticLevel) + { + properties = properties.SetItem(ProjectItemMetadata.DiagnosticLevel, foundLevel.ToString()); + } + + bool isImplicit = IsImplicit(projectFullPath, evaluationProperties); // When we only have evaluation data, mark the dependency as resolved if we currently have a corresponding resolved item diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/IDependencyNugetUpdateBlock.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/IDependencyNugetUpdateBlock.cs new file mode 100644 index 00000000000..da41a217edf --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/IDependencyNugetUpdateBlock.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.VisualStudio.Composition; +using Microsoft.VisualStudio.ProjectSystem.Tree.Dependencies.Models; + +namespace Microsoft.VisualStudio.ProjectSystem.Tree; + +[ProjectSystemContract(ProjectSystemContractScope.UnconfiguredProject, ProjectSystemContractProvider.Private, Cardinality = ImportCardinality.ExactlyOne)] +internal interface IDependencyNugetUpdateBlock : IProjectValueDataSource> +{ +}