Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c97a13d
[Feature] Check PSEdition compatibility for System32-path modules
Jun 22, 2018
ee98971
Merge branch 'master' into respect-compatiblepseditions-system32
rjmholt Jul 3, 2018
9b752c2
[Feature] Remove autocompletions for System32 modules, add edition ch…
Jul 3, 2018
ed14c89
Merge branch 'respect-compatiblepseditions-system32' of https://githu…
Jul 3, 2018
5535978
[Feature] Correct completion logic, prevent module loading for comple…
Jul 3, 2018
d8779c6
[Feature] Use Enum.HasFlags, rename test file to execute after GAC test
Jul 4, 2018
f7cba10
[Feature] Move disabled GAC test to new process
rjmholt Jul 5, 2018
eea20c3
[Feature] Tag GAC loading tests as 'Feature'
rjmholt Jul 5, 2018
f31cfcb
[Feature] Address @daxian-dbw's comments
Jul 6, 2018
eba3949
[Feature] Add comment to explain new process for GAC loading test
Jul 9, 2018
ee167a5
[Feature] Add test for PowerShell subprocesses
Jul 9, 2018
94394aa
[Feature] Skip powershell.exe tests on non-windows
Jul 9, 2018
ab70254
[Feature] Address feedback from @daxian-dbw
Jul 11, 2018
9860817
[Feature] Use PSModuleInfo.IsConsideredEditionCompatible
Jul 12, 2018
03a239c
Make IsOnSystem32ModulePath() trivially supported on UNIX
rjmholt Jul 12, 2018
0dc4be2
[Feature] Use LINQ filter for Get-Module
Jul 12, 2018
78f33be
[Feature] Add comment explaining BaseSkipEditionCheck in Test-ModuleM…
Jul 12, 2018
5ccab3c
[Feature] Stop Get-Module -List -All from returning incompatible nest…
Jul 12, 2018
ae6501a
[Feature] Add comment explaining module file filtering
Jul 12, 2018
02b19d0
[Feature] Fix compatibility logic, add Get-Module -List -All tests
Jul 13, 2018
95f2c8e
[Feature] Add -ErrorAction Stop to Get-Module tests for correct behav…
Jul 13, 2018
e708507
[Feature] Skip powershell subprocess tests on UNIX
Jul 13, 2018
8f835d0
[Feature] Skip BeforeAll blocks of Windows-only describes
Jul 13, 2018
f619d15
Merge branch 'master' into respect-compatiblepseditions-system32
rjmholt Jul 13, 2018
fc7d33b
[Feature] Remove duplicate test file, add more granular test for Get-…
Jul 13, 2018
90eb1d0
Merge branch 'respect-compatiblepseditions-system32' of https://githu…
Jul 13, 2018
d9b0f9d
[Feature] Factor out module setup into functions
Jul 13, 2018
073800e
[Feature] Address @daxian-dbw's comments
Jul 13, 2018
f8ccb9f
[Feature] Refactor PSEdition support check into static ModuleUtils me…
Jul 14, 2018
26af658
[Feature] Fix refactor bug, add Get-Module tests
Jul 14, 2018
21f287a
Merge branch 'master' into respect-compatiblepseditions-system32
rjmholt Jul 14, 2018
3c59b55
[Feature] Add comment to CompatiblePSEditions to document field
Jul 14, 2018
a0e1ca4
[Feature] Add Import-Module tests for nested modules
Jul 14, 2018
81cb110
[Feature] Add comment in PSModuleInfo about loading psm1s from System…
rjmholt Jul 15, 2018
87a6c32
[Feature] Skip Before/AfterAll blocks on UNIX
rjmholt Jul 15, 2018
6d72598
[Feature] Make PSModuleInfo.IsConsideredEditionCompatible not depend …
Jul 16, 2018
d0ac17a
[Feature] Fix Get-Module bug where loaded incompatible modules aren't…
Jul 16, 2018
e434a14
[Feature] Revert changes to Get-Module -ListAvailable -All
Jul 16, 2018
020123e
[Feature] Remove unused method
Jul 16, 2018
a034a4a
[Feature] Remove unused using directives from ModuleUtils.cs
daxian-dbw Jul 16, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1196,11 +1196,24 @@ private static IEnumerable<FormatViewDefinition> ViewsOf_ModuleInfoGrouping(Cust
.AddHeader(Alignment.Left, width: 10)
.AddHeader(Alignment.Left, width: 10)
.AddHeader(Alignment.Left, width: 35)
.AddHeader(Alignment.Left, width: 9, label: "PSEdition")
.AddHeader(Alignment.Left, label: "ExportedCommands")
.StartRowDefinition()
.AddPropertyColumn("ModuleType")
.AddPropertyColumn("Version")
.AddPropertyColumn("Name")
.AddScriptBlockColumn(@"
$result = [System.Collections.ArrayList]::new()
$editions = $_.CompatiblePSEditions
if (-not $editions)
{
$editions = @('Desktop')
}
foreach ($edition in $editions)
{
$result += $edition.Substring(0,4)
}
($result | Sort-Object) -join ','")
.AddScriptBlockColumn("$_.ExportedCommands.Keys")
.EndRowDefinition()
.EndTable());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun

#region Module Names

internal static List<CompletionResult> CompleteModuleName(CompletionContext context, bool loadedModulesOnly)
internal static List<CompletionResult> CompleteModuleName(CompletionContext context, bool loadedModulesOnly, bool skipEditionCheck = false)
{
var moduleName = context.WordToComplete ?? string.Empty;
var result = new List<CompletionResult>();
Expand All @@ -413,6 +413,12 @@ internal static List<CompletionResult> CompleteModuleName(CompletionContext cont
if (!loadedModulesOnly)
{
powershell.AddParameter("ListAvailable", true);

// -SkipEditionCheck should only be set or apply to -ListAvailable
if (skipEditionCheck)
{
powershell.AddParameter("SkipEditionCheck", true);
}
}

Exception exceptionThrown;
Expand Down Expand Up @@ -2101,17 +2107,19 @@ private static void NativeCommandArgumentCompletion(
case "Get-Module":
{
bool loadedModulesOnly = boundArguments == null || !boundArguments.ContainsKey("ListAvailable");
NativeCompletionModuleCommands(context, parameterName, loadedModulesOnly, /* isImportModule: */ false, result);
bool skipEditionCheck = !loadedModulesOnly && boundArguments.ContainsKey("SkipEditionCheck");
NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly, skipEditionCheck: skipEditionCheck);
break;
}
case "Remove-Module":
{
NativeCompletionModuleCommands(context, parameterName, /* loadedModulesOnly: */ true, /* isImportModule: */ false, result);
NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly: true);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Named parameters look more readable (what is "result" here?).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very much agree with you here. Sadly the parameter name is also result.

break;
}
case "Import-Module":
{
NativeCompletionModuleCommands(context, parameterName, /* loadedModulesOnly: */ false, /* isImportModule: */ true, result);
bool skipEditionCheck = boundArguments != null && boundArguments.ContainsKey("SkipEditionCheck");
NativeCompletionModuleCommands(context, parameterName, result, isImportModule: true, skipEditionCheck: skipEditionCheck);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Named parameters look more readable (what is "result" here?).

break;
}
case "Debug-Process":
Expand Down Expand Up @@ -3046,7 +3054,13 @@ private static void NativeCompletionScheduledJobCommands(CompletionContext conte
}
}

private static void NativeCompletionModuleCommands(CompletionContext context, string paramName, bool loadedModulesOnly, bool isImportModule, List<CompletionResult> result)
private static void NativeCompletionModuleCommands(
CompletionContext context,
string paramName,
List<CompletionResult> result,
bool loadedModulesOnly = false,
bool isImportModule = false,
bool skipEditionCheck = false)
{
if (string.IsNullOrEmpty(paramName))
{
Expand All @@ -3067,7 +3081,7 @@ private static void NativeCompletionModuleCommands(CompletionContext context, st
StringLiterals.PowerShellILAssemblyExtension,
StringLiterals.PowerShellCmdletizationFileExtension
};
var moduleFilesResults = new List<CompletionResult>(CompleteFilename(context, /* containerOnly: */ false, moduleExtensions));
var moduleFilesResults = new List<CompletionResult>(CompleteFilename(context, containerOnly: false, moduleExtensions));
if (moduleFilesResults.Count > 0)
result.AddRange(moduleFilesResults);

Expand All @@ -3079,7 +3093,7 @@ private static void NativeCompletionModuleCommands(CompletionContext context, st
}
}

var moduleResults = CompleteModuleName(context, loadedModulesOnly);
var moduleResults = CompleteModuleName(context, loadedModulesOnly, skipEditionCheck);
if (moduleResults != null && moduleResults.Count > 0)
result.AddRange(moduleResults);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ private static ConcurrentDictionary<string, CommandTypes> AnalyzeManifestModule(
var moduleManifestProperties = PsUtils.GetModuleManifestProperties(modulePath, PsUtils.FastModuleManifestAnalysisPropertyNames);
if (moduleManifestProperties != null)
{
if (ModuleIsEditionIncompatible(modulePath, moduleManifestProperties))
{
ModuleIntrinsics.Tracer.WriteLine($"Module lies on the Windows System32 legacy module path and is incompatible with current PowerShell edition, skipping module: {modulePath}");
return null;
}

Version version;
if (ModuleUtils.IsModuleInVersionSubdirectory(modulePath, out version))
{
Expand Down Expand Up @@ -160,6 +166,31 @@ private static ConcurrentDictionary<string, CommandTypes> AnalyzeManifestModule(
return result ?? AnalyzeTheOldWay(modulePath, context, lastWriteTime);
}

/// <summary>
/// Check if a module is compatible with the current PSEdition given its path and its manifest properties.
/// </summary>
/// <param name="modulePath">The path to the module.</param>
/// <param name="moduleManifestProperties">The properties of the module's manifest.</param>
/// <returns></returns>
internal static bool ModuleIsEditionIncompatible(string modulePath, Hashtable moduleManifestProperties)
{
#if UNIX
return false;
#else
if (!ModuleUtils.IsOnSystem32ModulePath(modulePath))
{
return false;
}

if (!moduleManifestProperties.ContainsKey("CompatiblePSEditions"))
{
return true;
}

return !Utils.IsPSEditionSupported(LanguagePrimitives.ConvertTo<string[]>(moduleManifestProperties["CompatiblePSEditions"]));
#endif
}

internal static bool ModuleAnalysisViaGetModuleRequired(object modulePathObj, bool hadCmdlets, bool hadFunctions, bool hadAliases)
{
var modulePath = modulePathObj as string;
Expand Down Expand Up @@ -449,6 +480,14 @@ internal static void CacheModuleExports(PSModuleInfo module, ExecutionContext co
{
ModuleIntrinsics.Tracer.WriteLine("Requested caching for {0}", module.Name);

// Don't cache incompatible modules on the system32 module path even if loaded with
// -SkipEditionCheck, since it will break subsequent sessions.
if (!module.IsConsideredEditionCompatible)
{
ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached.");
return;
}

DateTime lastWriteTime;
ModuleCacheEntry moduleCacheEntry;
GetModuleEntryFromCache(module.Path, out lastWriteTime, out moduleCacheEntry);
Expand Down
129 changes: 96 additions & 33 deletions src/System.Management.Automation/engine/Modules/GetModuleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ public sealed class GetModuleCommand : ModuleCmdletBase, IDisposable
[ArgumentCompleter(typeof(PSEditionArgumentCompleter))]
public string PSEdition { get; set; }

/// <summary>
/// When set, CompatiblePSEditions checking is disabled for modules in the System32 (Windows PowerShell) module directory.
/// </summary>
[Parameter(ParameterSetName = ParameterSet_AvailableLocally)]
[Parameter(ParameterSetName = ParameterSet_AvailableInPsrpSession)]
[Parameter(ParameterSetName = ParameterSet_AvailableInCimSession)]
public SwitchParameter SkipEditionCheck
{
get { return (SwitchParameter)BaseSkipEditionCheck; }
set { BaseSkipEditionCheck = value; }
}

/// <summary>
/// If specified, then Get-Module refreshes the internal cmdlet analysis cache
/// </summary>
Expand Down Expand Up @@ -341,6 +353,18 @@ protected override void ProcessRecord()
ThrowTerminatingError(error);
}

// -SkipEditionCheck only makes sense for -ListAvailable (otherwise the module is already loaded)
if (SkipEditionCheck && !ListAvailable)
{
ErrorRecord error = new ErrorRecord(
new InvalidOperationException(Modules.SkipEditionCheckNotSupportedWithoutListAvailable),
nameof(Modules.SkipEditionCheckNotSupportedWithoutListAvailable),
ErrorCategory.InvalidOperation,
targetObject: null);

ThrowTerminatingError(error);
}

var strNames = new List<string>();
if (Name != null)
{
Expand Down Expand Up @@ -423,27 +447,12 @@ private void AssertNameDoesNotResolveToAPath(string[] names, string stringFormat
}
}

/// <summary>
/// Determine whether a module info matches a given module specification table and specified PSEdition value.
/// </summary>
/// <param name="moduleInfo"></param>
/// <param name="moduleSpecTable"></param>
/// <param name="edition"></param>
/// <returns></returns>
private static bool ModuleMatch(PSModuleInfo moduleInfo, IDictionary<string, ModuleSpecification> moduleSpecTable, string edition)
{
ModuleSpecification moduleSpecification;
return (String.IsNullOrEmpty(edition) || moduleInfo.CompatiblePSEditions.Contains(edition, StringComparer.OrdinalIgnoreCase)) &&
(!moduleSpecTable.TryGetValue(moduleInfo.Name, out moduleSpecification) || ModuleIntrinsics.IsModuleMatchingModuleSpec(moduleInfo, moduleSpecification));
}

private void GetAvailableViaCimSession(IEnumerable<string> names, IDictionary<string, ModuleSpecification> moduleSpecTable,
CimSession cimSession, Uri resourceUri, string cimNamespace)
{
var remoteModules = GetAvailableViaCimSessionCore(names, cimSession, resourceUri, cimNamespace);
IEnumerable<PSModuleInfo> remoteModules = GetAvailableViaCimSessionCore(names, cimSession, resourceUri, cimNamespace);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this from var?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can change it back if desired, but my reasoning is:

  • The return type of GetAvailableViaCimSessionCore(...) isn't inferable with only that line, or even only that file
  • Reading the code without an IDE is something we have to do relatively often and is impaired by non-obvious var
  • Being explicit about what's expected as a return type means:
    • Compile-time errors from any changes occur on that line (I've seen type inference push them into functions and different files)
    • There's a type-as-contract concept at work, which explicitly defines what's expected and promotes superclasses as a sort of "contract loosening"

But that is definitely all opinion! As I say, I can revert it if the previous style is preferred.


foreach (var remoteModule in remoteModules.Where(remoteModule => ModuleMatch(remoteModule, moduleSpecTable, PSEdition))
)
foreach (PSModuleInfo remoteModule in FilterModulesForEditionAndSpecification(remoteModules, moduleSpecTable))
{
RemoteDiscoveryHelper.AssociatePSModuleInfoWithSession(remoteModule, cimSession, resourceUri,
cimNamespace);
Expand All @@ -453,10 +462,9 @@ private void GetAvailableViaCimSession(IEnumerable<string> names, IDictionary<st

private void GetAvailableViaPsrpSession(string[] names, IDictionary<string, ModuleSpecification> moduleSpecTable, PSSession session)
{
var remoteModules = GetAvailableViaPsrpSessionCore(names, session.Runspace);
IEnumerable<PSModuleInfo> remoteModules = GetAvailableViaPsrpSessionCore(names, session.Runspace);

foreach (var remoteModule in remoteModules.Where(remoteModule => ModuleMatch(remoteModule, moduleSpecTable, PSEdition))
)
foreach (PSModuleInfo remoteModule in FilterModulesForEditionAndSpecification(remoteModules, moduleSpecTable))
{
RemoteDiscoveryHelper.AssociatePSModuleInfoWithSession(remoteModule, session);
this.WriteObject(remoteModule);
Expand All @@ -465,14 +473,10 @@ private void GetAvailableViaPsrpSession(string[] names, IDictionary<string, Modu

private void GetAvailableLocallyModules(string[] names, IDictionary<string, ModuleSpecification> moduleSpecTable, bool all)
{
var refresh = Refresh.IsPresent;
var modules = GetModule(names, all, refresh);

foreach (
var psModule in
modules.Where(module => ModuleMatch(module, moduleSpecTable, PSEdition)).Select(module => new PSObject(module))
)
IEnumerable<PSModuleInfo> modules = GetModule(names, all, Refresh);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we use IEnumerable? We could use List - seems it is more thriftily.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used IEnumerable here because:

  • The interface type defines all the functionality we need without added constraints
  • We don't require indexing or even size checking, just ordered enumeration
  • The method is free to implement the collection as it wishes

But, perhaps there are benefits to using a List or at least an IList?

  • Less type complexity (and binding to a concrete type not an issue with an internal API)?
  • Provides strictness and ordering guarantees

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see IEnumerable benefits:

  • it allows to implement generic algorithms. Seems we don't need this.
  • it allows to postpone the execution (and implement interruption of long excution). Seems we don't need this too.

I believe we get more simple (readable and debuggable) code with List or Collection.

foreach (PSModuleInfo module in FilterModulesForEditionAndSpecification(modules, moduleSpecTable))
{
var psModule = new PSObject(module);
psModule.TypeNames.Insert(0, "ModuleInfoGrouping");
WriteObject(psModule);
}
Expand All @@ -482,14 +486,74 @@ private void GetLoadedModules(string[] names, IDictionary<string, ModuleSpecific
{
var modulesToWrite = Context.Modules.GetModules(names, all);

foreach (
var moduleInfo in
modulesToWrite.Where(moduleInfo => ModuleMatch(moduleInfo, moduleSpecTable, PSEdition))
)
foreach (PSModuleInfo moduleInfo in FilterModulesForEditionAndSpecification(modulesToWrite, moduleSpecTable))
{
WriteObject(moduleInfo);
}
}

/// <summary>
/// Filter an enumeration of PowerShell modules based on the required PowerShell edition
/// and the module specification constraints set for each module (if any).
/// </summary>
/// <param name="modules">The modules to filter through.</param>
/// <param name="moduleSpecificationTable">Module constraints, keyed by module name, to filter modules of that name by.</param>
/// <returns>All modules from the original input that meet both any module edition and module specification constraints provided.</returns>
private IEnumerable<PSModuleInfo> FilterModulesForEditionAndSpecification(
IEnumerable<PSModuleInfo> modules,
IDictionary<string, ModuleSpecification> moduleSpecificationTable)
{
#if !UNIX
// Edition check only applies to Windows System32 module path
if (!SkipEditionCheck && ListAvailable && !All)
{
modules = modules.Where(module => module.IsConsideredEditionCompatible);
}
#endif

if (!String.IsNullOrEmpty(PSEdition))
{
modules = modules.Where(module => module.CompatiblePSEditions.Contains(PSEdition, StringComparer.OrdinalIgnoreCase));
}

if (moduleSpecificationTable != null && moduleSpecificationTable.Count > 0)
{
modules = FilterModulesForSpecificationMatch(modules, moduleSpecificationTable);
}

return modules;
}

/// <summary>
/// Take an enumeration of modules and only return those that match a specification
/// in the given specification table, or have no corresponding entry in the specification table.
/// </summary>
/// <param name="modules">The modules to filter by specification match.</param>
/// <param name="moduleSpecificationTable">The specification lookup table to filter the modules on.</param>
/// <returns>The modules that match their corresponding table entry, or which have no table entry.</returns>
private static IEnumerable<PSModuleInfo> FilterModulesForSpecificationMatch(
IEnumerable<PSModuleInfo> modules,
IDictionary<string, ModuleSpecification> moduleSpecificationTable)
{
Dbg.Assert(moduleSpecificationTable != null, $"Caller to verify that {nameof(moduleSpecificationTable)} is not null");
Dbg.Assert(moduleSpecificationTable.Count != 0, $"Caller to verify that {nameof(moduleSpecificationTable)} is not empty");

foreach (PSModuleInfo module in modules)
{
// No table entry means we return the module
if (!moduleSpecificationTable.TryGetValue(module.Name, out ModuleSpecification moduleSpecification))
{
yield return module;
continue;
}

// Modules with table entries only get returned if they match them
if (ModuleIntrinsics.IsModuleMatchingModuleSpec(module, moduleSpecification))
{
yield return module;
}
}
}
}

/// <summary>
Expand All @@ -513,5 +577,4 @@ public IEnumerable<CompletionResult> CompleteArgument(string commandName, string
}
}
}
} // Microsoft.PowerShell.Commands

}
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ public SwitchParameter Force
set { BaseForce = value; }
}

/// <summary>
/// Skips the check on CompatiblePSEditions for modules loaded from the System32 module path.
/// </summary>
[Parameter]
public SwitchParameter SkipEditionCheck
{
get { return (SwitchParameter)BaseSkipEditionCheck; }
set { BaseSkipEditionCheck = value; }
}

/// <summary>
/// This parameter causes the session state instance to be written...
/// </summary>
Expand Down
Loading