From aa7b9d29c585fe71c27a0b392c7414fd8cf28717 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 14 Jul 2022 14:50:42 -0700 Subject: [PATCH 1/2] add new dependency DiagnosticLevels for nuget package statuses --- .../Dependencies/Models/DependencyModel.cs | 7 +- .../Tree/Dependencies/Snapshot/Dependency.cs | 67 ++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) 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..d536134c73f 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 } /// 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..bbb1aa8c1ef 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; + } + + 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 From 5683ca96a35caecba72a670fbb880060e9f9a1f9 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Tue, 27 Sep 2022 14:53:30 -0700 Subject: [PATCH 2/2] Add nuget package status indicator proof-of-concept --- .../DependencyNugetUpdateBlock.cs | 132 ++++++++++++++++++ .../Models/DependenciesViewModelFactory.cs | 8 +- .../Dependencies/Models/DependencyModel.cs | 7 +- .../Tree/Dependencies/Snapshot/Dependency.cs | 2 +- .../RuleHandlers/PackageRuleHandler.cs | 16 ++- .../Tree/IDependencyNugetUpdateBlock.cs | 11 ++ 6 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/Dependencies/DependencyNugetUpdateBlock.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Tree/IDependencyNugetUpdateBlock.cs 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 d536134c73f..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 @@ -66,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 bbb1aa8c1ef..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 @@ -169,6 +169,7 @@ public ImageMoniker ExpandedIcon return IconSet.ExpandedIcon; } + // TODO update upgradeavailable/deprecation/vulnerability icons switch (DiagnosticLevel) { case DiagnosticLevel.UpgradeAvailable: @@ -184,7 +185,6 @@ public ImageMoniker ExpandedIcon default: throw new ArgumentOutOfRangeException(); } - } } 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> +{ +}