-
Notifications
You must be signed in to change notification settings - Fork 389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add nuget package status indicator proof-of-concept #8539
base: main
Are you sure you want to change the base?
Changes from all commits
aa7b9d2
bf5de26
5e28428
5683ca9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -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<Dictionary<string, DiagnosticLevel>>, IDependencyNugetUpdateBlock | ||||||
{ | ||||||
private int _sourceVersion; | ||||||
|
||||||
private IBroadcastBlock<IProjectVersionedValue<Dictionary<string, DiagnosticLevel>>> _broadcastBlock = null!; | ||||||
|
||||||
private IReceivableSourceBlock<IProjectVersionedValue<Dictionary<string, DiagnosticLevel>>> _publicBlock = null!; | ||||||
|
||||||
private Dictionary<string, DiagnosticLevel>? _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<IProjectVersionedValue<Dictionary<string, DiagnosticLevel>>> SourceBlock | ||||||
{ | ||||||
get | ||||||
{ | ||||||
EnsureInitialized(); | ||||||
return _publicBlock; | ||||||
} | ||||||
} | ||||||
|
||||||
protected override void Initialize() | ||||||
{ | ||||||
#pragma warning disable RS0030 | ||||||
base.Initialize(); | ||||||
#pragma warning restore RS0030 | ||||||
|
||||||
_broadcastBlock = DataflowBlockSlim.CreateBroadcastBlock<IProjectVersionedValue<Dictionary<string, DiagnosticLevel>>>(nameFormat: $"{nameof(DependencyNugetUpdateBlock)} {1}"); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
_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<string, DiagnosticLevel> GetNewValue() | ||||||
{ | ||||||
Dictionary<string, DiagnosticLevel> 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<string, DiagnosticLevel> newValue) | ||||||
{ | ||||||
// Add thread safety as needed. Make sure to never regress the data source version published | ||||||
if (!DictionaryEqualityComparer<string, DiagnosticLevel>.Instance.Equals(newValue, _lastPublishedValue)) // only publish if you have to | ||||||
{ | ||||||
_lastPublishedValue = newValue; | ||||||
_broadcastBlock.Post( | ||||||
new ProjectVersionedValue<Dictionary<string, DiagnosticLevel>>( | ||||||
newValue, | ||||||
ImmutableDictionary.Create<NamedIdentity, IComparable>().Add( | ||||||
DataSourceKey, | ||||||
_sourceVersion++))); | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
|
||
/// <summary> | ||
|
@@ -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, | ||
Comment on lines
+69
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about replacing the string literals with Consider in future that someone renames an enum member. It's not obvious that such a change would break our binding to the MSBuild item metadata contract. |
||
_ => DiagnosticLevel.None | ||
}; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
Comment on lines
+140
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clear (I know this is a draft) but we should consider adding unresolved/deprecation/vulnerability icon members to This comment is based on the assumption that we end up with package reference icons with new overlays for the three new states. For non-package dependencies we would likely re-use the warning/error icons for those diagnostic levels, as we don't expect to display them and therefore wouldn't ask the design team to produce such icons for us. Brief history detour: These icons used to separate the base icon from the overlay. However in 17.0 the design team wanted finer control over how the overlay appeared, and so baked a flattened form of each combination into its own icon. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @drewnoakes so what exactly would be requested to be created of the design team? Just the overlays with no nuget package icon? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For each new overlay, you'd need a version of it displayed on top of the NuGet icon. That is, baked into a single image for each overlay. |
||
} | ||
} | ||
|
||
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 | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -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<Dictionary<string, DiagnosticLevel>> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
{ | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.