-
Notifications
You must be signed in to change notification settings - Fork 385
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
Convert compatibility module to binary module, fix compatibility with Azure environments #1212
Conversation
@rjmholt I'm overall OK with the changes as they are mainly just in your module. But I think @JamesWTruher would be the better person to review the logic, etc. as he has more experience/knowledge in this area. |
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.
here's the first of my comments
/// Add path prefixes of modules to exclude. | ||
/// </summary> | ||
/// <param name="modulePrefixes">Path prefixes of modules to exclude.</param> | ||
public Builder ExcludedModulePathPrefixes(IReadOnlyCollection<string> modulePrefixes) |
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.
is there a reason why some of these aren't a void return? I see that you don't use the return value where you call it.
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.
Just a convention of the builder pattern I've always used, but looking at the wikipedia page it seems to be less of a thing in C#. I'll change it over to just use simple properties.
/// <returns>An object containing common feature compatibility information.</returns> | ||
public CommonPowerShellData GetCommonPowerShellData() | ||
{ | ||
CmdletInfo gcmInfo = _pwsh.AddCommand(PowerShellDataCollector.GcmInfo) |
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.
we really need a better way to do this (get a command)
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.
It's possible without the info object (since some commands might be loaded dynamically or are defined in script), but @daxian-dbw told me that providing the info object makes it faster.
_pwsh.Dispose(); | ||
_platformInfoCollector.Dispose(); | ||
_pwshDataCollector.Dispose(); | ||
} |
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.
does dispose order matter here?
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.
I never quite know about dispose order. Theoretically, since they are all co-dependent, the outer one should dispose the inner ones, but I tend to dispose from inside -> out. Perhaps it should be the other way round?
osData.DistributionId = lsbInfo["DistributionId"]; | ||
osData.DistributionVersion = lsbInfo["DistributionVersion"]; | ||
osData.DistributionPrettyName = lsbInfo["DistributionPrettyName"]; | ||
break; |
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.
no mac?
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.
Looking on macOS now, we might want an extra field for the retail version (i.e. the macOS version rather than the darwin version), but I'll add that later I think. For now, a macOS platform is identified concretely by macOS
and the darwin kernel version I think.
...Collector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs
Outdated
Show resolved
Hide resolved
|
||
try | ||
{ | ||
functionData.ParameterSets = GetParameterSets(function.ParameterSets); |
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.
same
/// </summary> | ||
/// <param name="variables">Variables to collect information on.</param> | ||
/// <returns>An array of the names of the variables.</returns> | ||
public string[] GetVariablesData(IEnumerable<PSVariable> variables) |
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.
GetVariablesName
?
for (int i = 0; i < outputTypeData.Length; i++) | ||
{ | ||
outputTypeData[i] = outputType[i].Type != null | ||
? TypeNaming.GetFullTypeName(outputType[i].Type) |
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.
i'm assuming this is where it can throw
private static ReadOnlySet<string> GetPowerShellCommonParameterNames() | ||
{ | ||
var set = new List<string>(); | ||
foreach (PropertyInfo property in typeof(CommonParameters).GetProperties()) |
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.
GetProperties(Public,Instance,DeclaredOnly)
?
@@ -106,7 +187,7 @@ public static class TypeDataConversion | |||
|
|||
Type[] types = asm.GetTypes(); | |||
JsonDictionary<string, JsonDictionary<string, TypeData>> namespacedTypes = null; | |||
if (types.Any()) | |||
if (types.Length > 0) |
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.
in line 193, why not use types
from line 188?
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.
Good catch!
/// Add path prefixes of modules to exclude. | ||
/// </summary> | ||
/// <param name="modulePrefixes">Path prefixes of modules to exclude.</param> | ||
public Builder ExcludedModulePathPrefixes(IReadOnlyCollection<string> modulePrefixes) |
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.
Just a convention of the builder pattern I've always used, but looking at the wikipedia page it seems to be less of a thing in C#. I'll change it over to just use simple properties.
/// <returns>An object containing common feature compatibility information.</returns> | ||
public CommonPowerShellData GetCommonPowerShellData() | ||
{ | ||
CmdletInfo gcmInfo = _pwsh.AddCommand(PowerShellDataCollector.GcmInfo) |
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.
It's possible without the info object (since some commands might be loaded dynamically or are defined in script), but @daxian-dbw told me that providing the info object makes it faster.
_pwsh.Dispose(); | ||
_platformInfoCollector.Dispose(); | ||
_pwshDataCollector.Dispose(); | ||
} |
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.
I never quite know about dispose order. Theoretically, since they are all co-dependent, the outer one should dispose the inner ones, but I tend to dispose from inside -> out. Perhaps it should be the other way round?
osData.DistributionId = lsbInfo["DistributionId"]; | ||
osData.DistributionVersion = lsbInfo["DistributionVersion"]; | ||
osData.DistributionPrettyName = lsbInfo["DistributionPrettyName"]; | ||
break; |
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.
Looking on macOS now, we might want an extra field for the retail version (i.e. the macOS version rather than the darwin version), but I'll add that later I think. For now, a macOS platform is identified concretely by macOS
and the darwin kernel version I think.
|
||
private const string THIS_MODULE_NAME = "PSCompatibilityCollector"; | ||
|
||
private static readonly Regex s_typeDataRegex = new Regex("Error in TypeData \"([A-Za-z.]+)\"", RegexOptions.Compiled); |
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.
Good catch. There's a kind of error PowerShell emits when two modules have conflicting TypeData and the only way to identify it is by parsing the error message.
// Get default variables and core aliases out of a fresh runspace | ||
using (SMA.PowerShell freshPwsh = SMA.PowerShell.Create(RunspaceMode.NewRunspace)) | ||
{ | ||
Collection<PSVariable> defaultVariables = freshPwsh.AddCommand("Get-ChildItem") |
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.
Not a bad idea
|
||
try | ||
{ | ||
functionData.OutputType = GetOutputType(function.OutputType); |
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.
Actually, I think this occurs at the function.OutputType
point, when we fire the getter, because it has to find the type
|
||
try | ||
{ | ||
functionData.OutputType = GetOutputType(function.OutputType); |
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.
I'm going to leave this one for now. The real question in my mind was how much data do we want to record about the function that throws when we try to load information about it. But for now I think just the skeleton is ok.
@@ -106,7 +187,7 @@ public static class TypeDataConversion | |||
|
|||
Type[] types = asm.GetTypes(); | |||
JsonDictionary<string, JsonDictionary<string, TypeData>> namespacedTypes = null; | |||
if (types.Any()) | |||
if (types.Length > 0) |
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.
Good catch!
/// <summary> | ||
/// The verison of the profile schema this profile object uses. | ||
/// </summary> | ||
[DataMember] |
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.
I tried a few ways to set the automatic value of this to 1.0, but I couldn't work out how; I can't specify a version in the attribute.
/// If set, do not include whitespace like indentation or newlines in the output JSON. | ||
/// </summary> | ||
[Parameter] | ||
[Alias("Compress")] |
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.
do you think this alias will suggest that it will create an actual compressed (.zip) file?
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.
I actually put this in because ConvertTo-Json calls this Compress
, but left it an alias because I think it's a bad name for it
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.
Happy to go either way on this btw
return; | ||
|
||
default: | ||
throw new ArgumentException($"Unsupported type for {nameof(JsonSource)} parameter. Should be a string, FileInfo or TextReader object."); |
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.
I think this should probably be a WriteError
because it takes a collection of objects so one bad one should not cause the pipeline to abort. Also, if it should be terminating, then you probably want ThrowTerminatingError
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.
Ah ok! Is it possible to throw multiple terminating errors?
[Parameter] | ||
public DotnetData DotNet { get; set; } | ||
|
||
protected override void EndProcessing() |
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.
do you see a scenario where this would be called iteratively? If so, it should probably be in ProcessRecord
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.
I thought about that, but because there are no pipeline parameters and it's probably used for collecting a profile on the local machine, EndProcessing
seemed like the right call. This is effectively a function rather than a filter
} | ||
|
||
// If PassThru is set, just pass the object back and we're done | ||
if (PassThru) |
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.
PassThru
is usually used more like /bin/tee
where the data is returned and the targets are created.
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.
Yeah I was thinking about that. Maybe I should change this parameter name? Nothing else common really captures the behaviour though
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.
yah, probably, but later before we release and have time to think of a better parameter name
using System.Collections; | ||
using System.Collections.Generic; | ||
|
||
namespace Microsoft.PowerShell.CrossCompatibility |
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.
I wonder if this has enough utility to be part of PowerShell (or .NET)
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.
It really should be somewhere, I've wanted it a few times
|
||
namespace Microsoft.PowerShell.CrossCompatibility.Utility | ||
{ | ||
internal static class PowerShellExtensions |
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.
I really think this should be in PowerShell (it doesn't much help with down level), but it's a common pattern which we could easily improve for folks.
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.
Agreed. I'm not really sure how you're supposed to reuse a PowerShell instance without this. As in, I don't really understand the default scenario.
/// <summary> | ||
/// Class defining the Assert-PSCompatibilityProfileIsValid cmdlet. | ||
/// </summary> | ||
[Cmdlet(VerbsLifecycle.Assert, CommandUtilities.MODULE_PREFIX + "ProfileIsValid")] |
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.
it's also possible this could be the verb Test
and return true
or false
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.
Yeah I was weighing up those two. The problem is always that a boolean gives no diagnosis. I don't like that it throws an exception really, but it's better than a third custom solution
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.
good to go
PR Summary
Resolves #1149.
The original compatibility check module was hastily implemented and was quite slow. It also needed CIM/WMI to be available. This meant it didn't work nicely with things like Azure Functions.
I rewrote it as a binary to be faster and to have a simpler, more composable, more semantically definite .NET API.
There are a bunch of breaking changes with the compatibility types here (which are technically exposed by PSSA). There are also huge breaking changes in the PSCompatibilityAnalyzer module (I got rid of most of the commands), but that has never been released and is still below version 1.0, so that seems like less of a concern.
Basically there are no changes that break the PowerShell API of PSScriptAnalyzer unless people have gone hunting for types that are loaded (but aren't used directly by the module).
Also renames the PSCompatibilityAnalyzer module to PSCompatibilityCollector since that's more accurate and less confusing.
List of changes
ConstituentProfiles
field to be aReadOnlySet<string>
instead of aHashSet<string>
Microsoft.PowerShell.CrossCompatibility.Utility
namespace and into the base namespace,Collection
orRetrieval
depending on what the class does, since I thinkUtility
should be reserved for internal helpersPR Checklist
.cs
,.ps1
and.psm1
files have the correct copyright headerWIP:
to the beginning of the title and remove the prefix when the PR is ready.