From c97a13df7286dfddb26a65cce93cbebb7bcca4d8 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 22 Jun 2018 16:34:23 -0700 Subject: [PATCH 01/35] [Feature] Check PSEdition compatibility for System32-path modules * Add %WINDIR%\System32\WindowsPowerShell\v1.0\Modules to the PSModulePath by default * Add CompatiblePSEditions checks for modules loaded from the System32 path * Make modules with incompatible PSEditions loaded from System32 path fail to load * Make Get-Module -ListAvailable not list edition-incompatible modules from the System32 module path * Add -SkipEditionCheck switch to Get-Module and Import-Module to skip edition checking and show/load edition-incompatible modules respectively * Create tests for System32 module path compatibility checking * Make incompatible modules not autoload or autocomplete * Prevent caching of incompatible modules --- .../PowerShellCore_format_ps1xml.cs | 13 + .../engine/Modules/GetModuleCommand.cs | 161 ++++++++--- .../engine/Modules/ImportModuleCommand.cs | 10 + .../engine/Modules/ModuleCmdletBase.cs | 117 +++++++- .../engine/Modules/ModuleIntrinsics.cs | 29 ++ .../engine/Modules/ModuleUtils.cs | 70 ++++- .../engine/Modules/PSModuleInfo.cs | 14 +- .../engine/Utils.cs | 27 +- .../resources/Modules.resx | 3 + .../Import-Module.Tests.ps1 | 4 +- .../ModuleCompatiblePSEditions.Tests.ps1 | 249 ++++++++++++++++++ .../engine/Module/ModulePath.Tests.ps1 | 42 ++- 12 files changed, 679 insertions(+), 60 deletions(-) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs index 682acddb14d..5159a79a4e4 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs @@ -1192,11 +1192,24 @@ private static IEnumerable 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()); diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index a1bce6c9a27..7d66bfad8e7 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -84,6 +84,16 @@ public sealed class GetModuleCommand : ModuleCmdletBase, IDisposable [ArgumentCompleter(typeof(PSEditionArgumentCompleter))] public string PSEdition { get; set; } + /// + /// When set, CompatiblePSEditions checking is disabled for modules in the System32 (Windows PowerShell) module directory. + /// + [Parameter] + public SwitchParameter SkipEditionCheck + { + get { return (SwitchParameter)BaseSkipEditionCheck; } + set { BaseSkipEditionCheck = value; } + } + /// /// If specified, then Get-Module refreshes the internal cmdlet analysis cache /// @@ -423,27 +433,12 @@ private void AssertNameDoesNotResolveToAPath(string[] names, string stringFormat } } - /// - /// Determine whether a module info matches a given module specification table and specified PSEdition value. - /// - /// - /// - /// - /// - private static bool ModuleMatch(PSModuleInfo moduleInfo, IDictionary 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 names, IDictionary moduleSpecTable, CimSession cimSession, Uri resourceUri, string cimNamespace) { - var remoteModules = GetAvailableViaCimSessionCore(names, cimSession, resourceUri, cimNamespace); + IEnumerable remoteModules = GetAvailableViaCimSessionCore(names, cimSession, resourceUri, cimNamespace); - foreach (var remoteModule in remoteModules.Where(remoteModule => ModuleMatch(remoteModule, moduleSpecTable, PSEdition)) - ) + foreach (PSModuleInfo remoteModule in FilterModulesForEditionAndSpecification(remoteModules, moduleSpecTable)) { RemoteDiscoveryHelper.AssociatePSModuleInfoWithSession(remoteModule, cimSession, resourceUri, cimNamespace); @@ -453,10 +448,9 @@ private void GetAvailableViaCimSession(IEnumerable names, IDictionary moduleSpecTable, PSSession session) { - var remoteModules = GetAvailableViaPsrpSessionCore(names, session.Runspace); + IEnumerable 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); @@ -465,14 +459,10 @@ private void GetAvailableViaPsrpSession(string[] names, IDictionary 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 modules = GetModule(names, all, Refresh); + foreach (PSModuleInfo module in FilterModulesForEditionAndSpecification(modules, moduleSpecTable)) { + var psModule = new PSObject(module); psModule.TypeNames.Insert(0, "ModuleInfoGrouping"); WriteObject(psModule); } @@ -482,14 +472,120 @@ private void GetLoadedModules(string[] names, IDictionary ModuleMatch(moduleInfo, moduleSpecTable, PSEdition)) - ) + foreach (PSModuleInfo moduleInfo in FilterModulesForEditionAndSpecification(modulesToWrite, moduleSpecTable)) { WriteObject(moduleInfo); } } + + /// + /// Filter an enumeration of PowerShell modules based on the required PowerShell edition + /// and the module specification constraints set for each module (if any). + /// + /// The modules to filter through. + /// Module constraints, keyed by module name, to filter modules of that name by. + /// All modules from the original input that meet both any module edition and module specification constraints provided. + private IEnumerable FilterModulesForEditionAndSpecification( + IEnumerable modules, + IDictionary moduleSpecificationTable) + { +// Edition check only applies to Windows +// System32 module path +#if !UNIX + if (!SkipEditionCheck) + { + modules = FilterModulesForIncompatibleOnEditionCheckedPath(modules); + } +#endif + + if (!String.IsNullOrEmpty(PSEdition)) + { + modules = FilterModulesForEdition(modules); + } + + if (moduleSpecificationTable != null && moduleSpecificationTable.Count > 0) + { + modules = FilterModulesForSpecificationMatch(modules, moduleSpecificationTable); + } + + return modules; + } + + /// + /// Filter out all modules on the PowerShell-edition-checked path that are incompatible with + /// the current PowerShell edition from a given enumeration of modules. + /// + /// The modules to filter incompatible examples from. + /// All modules that are either not on the checked path or are compatible with the current PowerShell edition. + private IEnumerable FilterModulesForIncompatibleOnEditionCheckedPath(IEnumerable modules) + { + Dbg.Assert(!SkipEditionCheck, $"Caller to verify that {nameof(SkipEditionCheck)} is false"); + + foreach (PSModuleInfo module in modules) + { + if (!module.IsLoadedFromCompatibilityCheckedPath) + { + yield return module; + continue; + } + + IEnumerable moduleCompatibleEditions = module.CompatiblePSEditions.Any() ? module.CompatiblePSEditions : DefaultCompatiblePSEditions; + if (Utils.IsPSEditionSupported(moduleCompatibleEditions)) + { + yield return module; + } + } + } + + /// + /// Filter an enumeration of modules based on what PowerShell editions they are compatible with. + /// If the PSEdition parameter has been given, filter modules based on that. + /// + /// The modules to filter by edition. + /// All modules meeting the PSEdition constraint. + private IEnumerable FilterModulesForEdition(IEnumerable modules) + { + Dbg.Assert(!String.IsNullOrEmpty(PSEdition), $"Caller to verify that {nameof(PSEdition)} is not null or empty"); + + foreach (PSModuleInfo module in modules) + { + if (module.CompatiblePSEditions.Contains(PSEdition, StringComparer.OrdinalIgnoreCase)) + { + yield return module; + } + } + } + + /// + /// 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. + /// + /// The modules to filter by specification match. + /// The specification lookup table to filter the modules on. + /// The modules that match their corresponding table entry, or which have no table entry. + private static IEnumerable FilterModulesForSpecificationMatch( + IEnumerable modules, + IDictionary 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; + } + } + } } /// @@ -513,5 +609,4 @@ public IEnumerable CompleteArgument(string commandName, string } } } -} // Microsoft.PowerShell.Commands - +} diff --git a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs index dfa78af0f13..885ec3483d4 100644 --- a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs @@ -207,6 +207,16 @@ public SwitchParameter Force set { BaseForce = value; } } + /// + /// Skips the check on CompatiblePSEditions for modules loaded from the System32 module path. + /// + [Parameter] + public SwitchParameter SkipEditionCheck + { + get { return (SwitchParameter)BaseSkipEditionCheck; } + set { BaseSkipEditionCheck = value; } + } + /// /// This parameter causes the session state instance to be written... /// diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 3885c5ddd71..e7020f96b1d 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -105,6 +105,11 @@ protected internal struct ImportModuleOptions /// internal bool BaseGlobal { get; set; } + /// + /// If set, CompatiblePSEditions checking will be disabled for modules on the System32 path. + /// + internal bool BaseSkipEditionCheck { get; set; } + internal SessionState TargetSessionState { get @@ -256,6 +261,16 @@ internal List MatchAll "ModuleVersion" }; + /// + /// When module manifests lack a CompatiblePSEditions field, + /// they will be treated as if they have this value. + /// The PSModuleInfo will still reflect the lack of value. + /// + internal static IReadOnlyList DefaultCompatiblePSEditions { get; } = new string[] + { + "Desktop" + }; + private Dictionary _currentlyProcessingModules = new Dictionary(); internal bool LoadUsingModulePath(bool found, IEnumerable modulePath, string name, SessionState ss, @@ -2361,6 +2376,51 @@ internal PSModuleInfo LoadModuleManifest( if (bailOnFirstError) return null; } + // On Windows, we want to include any modules under %WINDIR%\System32\WindowsPowerShell\v1.0\Modules + // that have declared compatibility with PS Core (or if the check is skipped) + IEnumerable inferredCompatiblePSEditions = compatiblePSEditions ?? DefaultCompatiblePSEditions; + if (!IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions, out bool isOnSystem32ModulePath)) + { + containedErrors = true; + + if (writingErrors) + { + message = StringUtil.Format( + Modules.PSEditionNotSupported, + moduleManifestPath, + PSVersionInfo.PSEdition, + String.Join(',', inferredCompatiblePSEditions)); + + InvalidOperationException ioe = new InvalidOperationException(message); + + ErrorRecord er = new ErrorRecord( + ioe, + nameof(Modules) + "_" + nameof(Modules.PSEditionNotSupported), + ErrorCategory.ResourceUnavailable, + moduleManifestPath); + + WriteError(er); + } + + if (bailOnFirstError) + { + // If we're trying to load the module, return null so that caches + // are not polluted + if ((manifestProcessingFlags & ManifestProcessingFlags.LoadElements) != 0) + { + return null; + } + + // If we return null with Get-Module, a fake module info will be created. Since + // we want to suppress output of the module, we need to do that here. + return new PSModuleInfo(moduleManifestPath, context: null, sessionState: null) + { + HadErrorsLoading = true, + IsLoadedFromCompatibilityCheckedPath = isOnSystem32ModulePath + }; + } + } + // Process format.ps1xml / types.ps1.xml / RequiredAssemblies // as late as possible, but before ModuleToProcess, ScriptToProcess, NestedModules if (importingModule) @@ -2482,13 +2542,18 @@ internal PSModuleInfo LoadModuleManifest( manifestInfo.AddToModuleList(m); } } + if (compatiblePSEditions != null) { - foreach (var psEdition in compatiblePSEditions) - { - manifestInfo.AddToCompatiblePSEditions(psEdition); - } + manifestInfo.AddToCompatiblePSEditions(compatiblePSEditions); } + +// Modules loaded from the System32 module path may +// not have the CompatiblePSEditions fields ignored +#if !UNIX + manifestInfo.IsLoadedFromCompatibilityCheckedPath = isOnSystem32ModulePath; +#endif + if (scriptsToProcess != null) { foreach (var s in scriptsToProcess) @@ -3052,6 +3117,12 @@ internal PSModuleInfo LoadModuleManifest( newManifestInfo.IconUri = manifestInfo.IconUri; newManifestInfo.RepositorySourceLocation = manifestInfo.RepositorySourceLocation; +// On Windows, we need to copy over the field indicating +// whether the module was imported from the System32 module path +#if !UNIX + newManifestInfo.IsLoadedFromCompatibilityCheckedPath = manifestInfo.IsLoadedFromCompatibilityCheckedPath; +#endif + // If we are in module discovery, then fix the path. if (ss == null) { @@ -3129,10 +3200,7 @@ internal PSModuleInfo LoadModuleManifest( { if (compatiblePSEditions != null) { - foreach (var psEdition in compatiblePSEditions) - { - newManifestInfo.AddToCompatiblePSEditions(psEdition); - } + newManifestInfo.AddToCompatiblePSEditions(compatiblePSEditions); } } @@ -3364,6 +3432,39 @@ internal PSModuleInfo LoadModuleManifest( return manifestInfo; } + /// + /// Check if the CompatiblePSEditions field of a given module + /// declares compatibility with the running PowerShell edition. + /// + /// The path to the module manifest being checked. + /// The value of the CompatiblePSEditions field of the module manifest. + /// + /// True if the module is being loaded from the Windows PowerShell $PSHOME module path (under %WINDIR%\System32), false otherwise. + /// + /// True if the module is compatible with the running PowerShell edition, false otherwise. + private bool IsPSEditionCompatible( + string moduleManifestPath, + IEnumerable compatiblePSEditions, + out bool isOnSystem32ModulePath) + { + isOnSystem32ModulePath = false; + +#if UNIX + return true; +#else + + string windowsPSModulePath = ModuleIntrinsics.GetWindowsPowerShellPSHomeModulePath(); + if (!moduleManifestPath.StartsWith(windowsPSModulePath, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + isOnSystem32ModulePath = true; + + return BaseSkipEditionCheck || Utils.IsPSEditionSupported(compatiblePSEditions); +#endif + } + private static void PropagateExportedTypesFromNestedModulesToRootModuleScope(ImportModuleOptions options, PSModuleInfo manifestInfo) { if (manifestInfo.NestedModules == null) diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 93a97302560..fe790ce3a65 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -30,6 +30,11 @@ public class ModuleIntrinsics [TraceSource("Modules", "Module loading and analysis")] internal static PSTraceSource Tracer = PSTraceSource.GetTracer("Modules", "Module loading and analysis"); + // The %WINDIR%\System32\WindowsPowerShell\v1.0\Modules module path, + // to load forward compatible Windows PowerShell modules from + private static readonly string s_windowsPowerShellPSHomeModulePath = + Path.Combine(System.Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "Modules"); + internal ModuleIntrinsics(ExecutionContext context) { _context = context; @@ -600,6 +605,23 @@ private static string GetSharedModulePath() #endif } +#if !UNIX + /// + /// Get the path to the Windows PowerShell module directory under the + /// System32 directory on Windows (the Windows PowerShell $PSHOME). + /// + /// The path of the Windows PowerShell system module directory. + internal static string GetWindowsPowerShellPSHomeModulePath() + { + if (!String.IsNullOrEmpty(InternalTestHooks.TestWindowsPowerShellPSHomeLocation)) + { + return InternalTestHooks.TestWindowsPowerShellPSHomeLocation; + } + + return s_windowsPowerShellPSHomeModulePath; + } +#endif + /// /// Combine the PS system-wide module path and the DSC module path /// to get the system module paths. @@ -984,6 +1006,13 @@ internal static string SetModulePath() if (!string.IsNullOrEmpty(newModulePathString)) { +// If on Windows, we want to add the System32 +// Windows PowerShell module directory +// so that Windows modules are discovered +#if !UNIX + newModulePathString += Path.PathSeparator + GetWindowsPowerShellPSHomeModulePath(); +#endif + // Set the environment variable... Environment.SetEnvironmentVariable(Constants.PSModulePathEnvVar, newModulePathString); } diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index ac76e426ea2..edb2a22c4e7 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Management.Automation.Runspaces; using Dbg = System.Management.Automation.Diagnostics; @@ -11,6 +13,11 @@ namespace System.Management.Automation.Internal { internal static class ModuleUtils { + // Cache for modules on the System32 module path, which are assumed to not be deleted or have their editions change. + // Null entries denote incompatible modules. + private static readonly ConcurrentDictionary s_incompatibleEditionSystem32Modules = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + internal static bool IsPossibleModuleDirectory(string dir) { // We shouldn't be searching in hidden directories. @@ -339,11 +346,11 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe ) ) { - foreach (string modulePath in GetDefaultAvailableModuleFiles(true, false, context)) + foreach (string modulePath in GetDefaultAvailableModuleFiles(force: true, isForAutoDiscovery: false, context)) { // Skip modules that have already been loaded so that we don't expose private commands. string moduleName = Path.GetFileNameWithoutExtension(modulePath); - var modules = context.Modules.GetExactMatchModules(moduleName, all: false, exactMatch: true); + List modules = context.Modules.GetExactMatchModules(moduleName, all: false, exactMatch: true); PSModuleInfo tempModuleInfo = null; if (modules.Count != 0) @@ -359,10 +366,10 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe if (modules.Count == 1) { PSModuleInfo psModule = modules[0]; - tempModuleInfo = new PSModuleInfo(psModule.Name, psModule.Path, null, null); + tempModuleInfo = new PSModuleInfo(psModule.Name, psModule.Path, context: null, sessionState: null); tempModuleInfo.SetModuleBase(psModule.ModuleBase); - foreach (var entry in psModule.ExportedCommands) + foreach (KeyValuePair entry in psModule.ExportedCommands) { if (commandPattern.IsMatch(entry.Value.Name)) { @@ -370,7 +377,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe switch (entry.Value.CommandType) { case CommandTypes.Alias: - current = new AliasInfo(entry.Value.Name, null, context); + current = new AliasInfo(entry.Value.Name, definition: null, context); break; case CommandTypes.Function: current = new FunctionInfo(entry.Value.Name, ScriptBlock.EmptyScriptBlock, context); @@ -382,7 +389,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe current = new ConfigurationInfo(entry.Value.Name, ScriptBlock.EmptyScriptBlock, context); break; case CommandTypes.Cmdlet: - current = new CmdletInfo(entry.Value.Name, null, null, null, context); + current = new CmdletInfo(entry.Value.Name, implementingType: null, helpFile: null, PSSnapin: null, context); break; default: Dbg.Assert(false, "cannot be hit"); @@ -399,11 +406,52 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe } string moduleShortName = System.IO.Path.GetFileNameWithoutExtension(modulePath); - var exportedCommands = AnalysisCache.GetExportedCommands(modulePath, false, context); + +// System32 module CompatiblePSEditions checks: +// incompatible modules should not appear as completions +#if !UNIX + // If the module is on the System32 path where CompatiblePSEditions are checked, + // we are forced to use Get-Module to discover the compatibility of the module + string psCompatibleEditionsCheckedPath = ModuleIntrinsics.GetWindowsPowerShellPSHomeModulePath(); + if (modulePath.StartsWith(psCompatibleEditionsCheckedPath, StringComparison.OrdinalIgnoreCase)) + { + // Do our best not to call Get-Module by using a cache + if (s_incompatibleEditionSystem32Modules.ContainsKey(modulePath)) + { + // We have already identified the module as incompatible + continue; + } + else + { + using (PowerShell pwsh = PowerShell.Create()) + { + PSModuleInfo module = pwsh.AddCommand("Get-Module") + .AddParameter("ListAvailable") + .AddParameter("SkipEditionCheck") + .AddArgument(modulePath) + .Invoke() + .FirstOrDefault(); + + if (module == null) + { + continue; + } + + if (!Utils.IsPSEditionSupported(module.CompatiblePSEditions)) + { + s_incompatibleEditionSystem32Modules[modulePath] = module; + continue; + } + } + } + } +#endif + + IDictionary exportedCommands = AnalysisCache.GetExportedCommands(modulePath, testOnly: false, context); if (exportedCommands == null) { continue; } - tempModuleInfo = new PSModuleInfo(moduleShortName, modulePath, null, null); + tempModuleInfo = new PSModuleInfo(moduleShortName, modulePath, sessionState: null, context: null); if (InitialSessionState.IsEngineModule(moduleShortName)) { tempModuleInfo.SetModuleBase(Utils.DefaultPowerShellAppBase); @@ -416,10 +464,10 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe tempModuleInfo.SetGuid(ModuleIntrinsics.GetManifestGuid(modulePath)); } - foreach (var pair in exportedCommands) + foreach (KeyValuePair pair in exportedCommands) { - var commandName = pair.Key; - var commandTypes = pair.Value; + string commandName = pair.Key; + CommandTypes commandTypes = pair.Value; if (commandPattern.IsMatch(commandName)) { diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index df216e5a515..d7135ed05ac 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -841,6 +841,18 @@ internal void AddToCompatiblePSEditions(string psEdition) _compatiblePSEditions.Add(psEdition); } + internal void AddToCompatiblePSEditions(IEnumerable psEditions) + { + _compatiblePSEditions.AddRange(psEditions); + } + + /// + /// Indicates whether this module has been loaded from a path where its compatibility should be checked. + /// If true, this module's CompatiblePSEditions should be checked against the current PowerShell Edition + /// before it is shown by cmdlets like Get-Module -ListAvailable. + /// + internal bool IsLoadedFromCompatibilityCheckedPath { get; set; } = false; + /// /// ModuleList /// @@ -1595,4 +1607,4 @@ public int GetHashCode(PSModuleInfo obj) } } } -} // System.Management.Automation \ No newline at end of file +} diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index f1a4bdc6c73..7efea5804ef 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -451,7 +451,7 @@ internal static bool IsPSVersionSupported(Version checkVersion) } /// - /// Checks whether current monad session supports edition specified + /// Checks whether current PowerShell session supports edition specified /// by checkEdition. /// /// Edition to check @@ -462,7 +462,26 @@ internal static bool IsPSEditionSupported(string checkEdition) } /// - /// Checks whether the specified edition values is allowed. + /// Check whether the current PowerShell session supports any of the specified editions. + /// + /// The PowerShell editions to check compatibility with. + /// True if the edition is supported by this runtime, false otherwise. + internal static bool IsPSEditionSupported(IEnumerable editions) + { + string currentPSEdition = PSVersionInfo.PSEdition; + foreach (string edition in editions) + { + if (currentPSEdition.Equals(edition, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether the specified edition value is allowed. /// /// Edition value to check /// true if allowed, false otherwise @@ -1449,6 +1468,10 @@ public static class InternalTestHooks internal static bool StopwatchIsNotHighResolution; internal static bool DisableGACLoading; + // A location to test PSEdition compatibility functionality for Windows PowerShell modules with + // since we can't manipulate the System32 directory in a test + internal static string TestWindowsPowerShellPSHomeLocation; + /// This member is used for internal test purposes. public static void SetTestHook(string property, object value) { diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index af889e15561..79267978969 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -606,4 +606,7 @@ This prerequisite is valid for the PowerShell Desktop edition only. + + Module '{0}' does not support current PowerShell edition '{1}'. Its supported editions are '{2}'. Use 'Import-Module -SkipEditionCheck' to ignore the compatibility of this module. + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 index 9a77234b768..74ea9c373df 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 @@ -111,14 +111,14 @@ Describe "Import-Module for Binary Modules in GAC" -Tags 'CI' { It "Load PSScheduledJob from Windows Powershell Modules folder should fail" -Skip:(-not $IsWindows) { $modulePath = Join-Path $env:windir "System32/WindowsPowershell/v1.0/Modules/PSScheduledJob" - { Import-Module $modulePath -ErrorAction SilentlyContinue } | Should -Throw -ErrorId 'FormatXmlUpdateException,Microsoft.PowerShell.Commands.ImportModuleCommand' + { Import-Module $modulePath -SkipEditionCheck -ErrorAction SilentlyContinue } | Should -Throw -ErrorId 'FormatXmlUpdateException,Microsoft.PowerShell.Commands.ImportModuleCommand' } } Context "Modules are loaded from GAC" { It "Load PSScheduledJob from Windows Powershell Modules folder" -Skip:(-not $IsWindows) { $modulePath = Join-Path $env:windir "System32/WindowsPowershell/v1.0/Modules/PSScheduledJob" - Import-Module $modulePath + Import-Module $modulePath -SkipEditionCheck (Get-Command New-JobTrigger).Name | Should -BeExactly 'New-JobTrigger' } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 new file mode 100644 index 00000000000..dc4c4225115 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 @@ -0,0 +1,249 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# NOTE: This test suite must be named/located so that it comes after the +# "Disable GAC loading" test in Import-Module.Tests.ps1. This is because that +# test will fail when the type it's trying to load is already in the cache and succeeds. + +$script:oldModulePath = $env:PSModulePath + +function Add-ModulePath +{ + param([string]$Path) + + $script:oldModulePath = $env:PSModulePath + + $env:PSModulePath = $env:PSModulePath + [System.IO.Path]::PathSeparator + $Path +} + +function Restore-ModulePath +{ + $env:PSModulePath = $script:oldModulePath +} + +# Creates a new dummy module compatible with the given PSEditions +function New-EditionCompatibleModule +{ + param( + [Parameter(Mandatory = $true)][string]$ModuleName, + [string]$DirPath, + [string[]]$CompatiblePSEditions) + + $modulePath = Join-Path $DirPath $ModuleName + + $manifestPath = Join-Path $modulePath "$ModuleName.psd1" + + $psm1Name = "$ModuleName.psm1" + $psm1Path = Join-Path $modulePath $psm1Name + + New-Item -Path $modulePath -ItemType Directory + + New-Item -Path $psm1Path -Value "function Test-$ModuleName { `$true }" + + if ($CompatiblePSEditions) + { + New-ModuleManifest -Path $manifestPath -CompatiblePSEditions $CompatiblePSEditions -RootModule $psm1Name + } + else + { + New-ModuleManifest -Path $manifestPath -RootModule $psm1Name + } + + return $modulePath +} + +function New-TestModules +{ + param([hashtable[]]$TestCases, [string]$BaseDir) + + for ($i = 0; $i -lt $TestCases.Count; $i++) + { + $path = New-EditionCompatibleModule -ModuleName $TestCases[$i].ModuleName -CompatiblePSEditions $TestCases[$i].Editions -Dir $BaseDir + + $TestCases[$i].Path = $path + $TestCases[$i].Name = $TestCases[$i].Editions -join "," + } +} + +Describe "Get-Module with CompatiblePSEditions-checked paths" -Tag "CI" { + + BeforeAll { + $successCases = @( + @{ Editions = "Core","Desktop"; ModuleName = "BothModule" }, + @{ Editions = "Core"; ModuleName = "CoreModule" } + ) + + $failCases = @( + @{ Editions = "Desktop"; ModuleName = "DesktopModule" }, + @{ Editions = $null; ModuleName = "NeitherModule" } + ) + + $basePath = Join-Path $TestDrive "EditionCompatibleModules" + New-TestModules -TestCases $successCases -BaseDir $basePath + New-TestModules -TestCases $failCases -BaseDir $basePath + + # Emulate the System32 module path for tests + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $basePath) + } + + AfterAll { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) + } + + Context "Loading from checked paths on the module path with no flags" { + BeforeAll { + Add-ModulePath $basePath + $modules = Get-Module -ListAvailable + } + + AfterAll { + Restore-ModulePath + } + + It "Lists compatible modules from the module path with -ListAvailable for PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules.Name | Should -Contain $ModuleName + } + + It "Does not list incompatible modules with -ListAvailable for PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules.Name | Should -Not -Contain $ModuleName + } + } + + Context "Loading from checked paths by absolute path with no flags" { + It "Lists compatible modules with -ListAvailable for PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules = Get-Module -ListAvailable (Join-Path -Path $basePath -ChildPath $ModuleName) + + $modules.Name | Should -Contain $ModuleName + } + + It "Does not list incompatible modules with -ListAvailable for PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules = Get-Module -ListAvailable (Join-Path -Path $basePath -ChildPath $ModuleName) + + $modules.Name | Should -Not -Contain $ModuleName + } + } + + Context "Loading from checked paths on the module path with -SkipEditionCheck" { + BeforeAll { + Add-ModulePath $basePath + $modules = Get-Module -ListAvailable -SkipEditionCheck + } + + AfterAll { + Restore-ModulePath + } + + It "Lists all modules from the module path with -ListAvailable for PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules.Name | Should -Contain $ModuleName + } + } + + Context "Loading from checked paths by absolute path with -SkipEditionCheck" { + It "Lists compatible modules with -ListAvailable for PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules = Get-Module -ListAvailable -SkipEditionCheck (Join-Path -Path $basePath -ChildPath $ModuleName) + + $modules.Name | Should -Contain $ModuleName + } + } +} + +Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { + BeforeAll { + $successCases = @( + @{ Editions = "Core","Desktop"; ModuleName = "BothModule"; Result = $true }, + @{ Editions = "Core"; ModuleName = "CoreModule"; Result = $true } + ) + + $failCases = @( + @{ Editions = "Desktop"; ModuleName = "DesktopModule"; Result = $true }, + @{ Editions = $null; ModuleName = "NeitherModule"; Result = $true } + ) + + $basePath = Join-Path $TestDrive "EditionCompatibleModules" + New-TestModules -TestCases $successCases -BaseDir $basePath + New-TestModules -TestCases $failCases -BaseDir $basePath + + $allModules = ($successCases + $failCases).ModuleName + + # Emulate the System32 module path for tests + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $basePath) + } + + AfterAll { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) + } + + AfterEach { + Get-Module $allModules | Remove-Module -Force + } + + Context "Imports from module path" { + BeforeAll { + Add-ModulePath $basePath + } + + AfterAll { + Restore-ModulePath + } + + It "Successfully imports compatible modules from the module path with PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + Import-Module $ModuleName -Force + & "Test-$ModuleName" | Should -Be $Result + } + + It "Fails to import incompatible modules from the module path with PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + { Import-Module $ModuleName -Force -ErrorAction 'Stop'; & "Test-$ModuleName" } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Imports an incompatible module from the module path with -SkipEditionCheck with PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + Import-Module $ModuleName -SkipEditionCheck -Force + & "Test-$ModuleName" | Should -Be $Result + } + } + + Context "Imports from absolute path" { + It "Successfully imports compatible modules from an absolute path with PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + $path = Join-Path -Path $basePath -ChildPath $ModuleName + + Import-Module $path -Force + & "Test-$ModuleName" | Should -Be $Result + } + + It "Fails to import incompatible modules from an absolute path with PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + $path = Join-Path -Path $basePath -ChildPath $ModuleName + + { Import-Module $path -Force -ErrorAction 'Stop'; & "Test-$ModuleName" } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Imports an incompatible module from an absolute path with -SkipEditionCheck with PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + $path = Join-Path -Path $basePath -ChildPath $ModuleName + + Import-Module $path -SkipEditionCheck -Force + & "Test-$ModuleName" | Should -Be $Result + } + } +} diff --git a/test/powershell/engine/Module/ModulePath.Tests.ps1 b/test/powershell/engine/Module/ModulePath.Tests.ps1 index 844ac97a7ef..19e78354dc4 100644 --- a/test/powershell/engine/Module/ModulePath.Tests.ps1 +++ b/test/powershell/engine/Module/ModulePath.Tests.ps1 @@ -23,6 +23,11 @@ Describe "SxS Module Path Basic Tests" -tags "CI" { } $expectedSystemPath = Join-Path -Path $PSHOME -ChildPath 'Modules' + if ($IsWindows) + { + $expectedWindowsPowerShellPSHomePath = Join-Path $env:windir "System32" "WindowsPowerShell" "v1.0" "Modules" + } + ## Setup a fake PSHome $fakePSHome = Join-Path -Path $TestDrive -ChildPath 'FakePSHome' $fakePSHomeModuleDir = Join-Path -Path $fakePSHome -ChildPath 'Modules' @@ -48,10 +53,22 @@ Describe "SxS Module Path Basic Tests" -tags "CI" { $paths = $defaultModulePath -split [System.IO.Path]::PathSeparator - $paths.Count | Should -Be 3 + if ($IsWindows) + { + $paths.Count | Should -Be 4 + } + else + { + $paths.Count | Should -Be 3 + } + $paths[0].TrimEnd([System.IO.Path]::DirectorySeparatorChar) | Should -Be $expectedUserPath $paths[1].TrimEnd([System.IO.Path]::DirectorySeparatorChar) | Should -Be $expectedSharedPath $paths[2].TrimEnd([System.IO.Path]::DirectorySeparatorChar) | Should -Be $expectedSystemPath + if ($IsWindows) + { + $paths[3].TrimEnd([System.IO.Path]::DirectorySeparatorChar) | Should -Be $expectedWindowsPowerShellPSHomePath + } } It "ignore pshome module path derived from a different powershell core instance" -Skip:(!$IsCoreCLR) { @@ -68,10 +85,22 @@ Describe "SxS Module Path Basic Tests" -tags "CI" { $newModulePath = & $powershell -nopro -c '$env:PSModulePath' $paths = $newModulePath -split [System.IO.Path]::PathSeparator - $paths.Count | Should -Be 3 + if ($IsWindows) + { + $paths.Count | Should -Be 4 + } + else + { + $paths.Count | Should -Be 3 + } + $paths[0] | Should -Be $expectedUserPath $paths[1] | Should -Be $expectedSharedPath $paths[2] | Should -Be $expectedSystemPath + if ($IsWindows) + { + $paths[3].TrimEnd([System.IO.Path]::DirectorySeparatorChar) | Should -Be $expectedWindowsPowerShellPSHomePath + } } finally { @@ -89,7 +118,14 @@ Describe "SxS Module Path Basic Tests" -tags "CI" { $newModulePath = & $powershell -nopro -c '$env:PSModulePath' $paths = $newModulePath -split [System.IO.Path]::PathSeparator - $paths.Count | Should -Be 5 + if ($IsWindows) + { + $paths.Count | Should -Be 6 + } + else + { + $paths.Count | Should -Be 5 + } $paths -contains $fakePSHomeModuleDir | Should -BeTrue $paths -contains $customeModules | Should -BeTrue } From 9b752c27c09a13b15f131bd4915a2d58bab3ece4 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Jul 2018 12:21:22 -0700 Subject: [PATCH 02/35] [Feature] Remove autocompletions for System32 modules, add edition check to Test-ModuleManifest --- .../engine/Modules/ModuleUtils.cs | 32 ++----------------- .../Modules/TestModuleManifestCommand.cs | 8 +++++ 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index edb2a22c4e7..64a8fe6ba30 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -411,39 +411,11 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe // incompatible modules should not appear as completions #if !UNIX // If the module is on the System32 path where CompatiblePSEditions are checked, - // we are forced to use Get-Module to discover the compatibility of the module + // we skip it -- only give completions for loaded modules on this path string psCompatibleEditionsCheckedPath = ModuleIntrinsics.GetWindowsPowerShellPSHomeModulePath(); if (modulePath.StartsWith(psCompatibleEditionsCheckedPath, StringComparison.OrdinalIgnoreCase)) { - // Do our best not to call Get-Module by using a cache - if (s_incompatibleEditionSystem32Modules.ContainsKey(modulePath)) - { - // We have already identified the module as incompatible - continue; - } - else - { - using (PowerShell pwsh = PowerShell.Create()) - { - PSModuleInfo module = pwsh.AddCommand("Get-Module") - .AddParameter("ListAvailable") - .AddParameter("SkipEditionCheck") - .AddArgument(modulePath) - .Invoke() - .FirstOrDefault(); - - if (module == null) - { - continue; - } - - if (!Utils.IsPSEditionSupported(module.CompatiblePSEditions)) - { - s_incompatibleEditionSystem32Modules[modulePath] = module; - continue; - } - } - } + continue; } #endif diff --git a/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs b/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs index d34665801f3..b897b7c739f 100644 --- a/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs +++ b/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs @@ -23,6 +23,14 @@ namespace Microsoft.PowerShell.Commands [OutputType(typeof(PSModuleInfo))] public sealed class TestModuleManifestCommand : ModuleCmdletBase { + /// + /// Creates an instance of the Test-ModuleManifest command + /// + public TestModuleManifestCommand() + { + BaseSkipEditionCheck = true; + } + /// /// The output path for the generated file... /// From 55359783ae81a728b9020f1c1b462df0e3da0c27 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Jul 2018 16:21:10 -0700 Subject: [PATCH 03/35] [Feature] Correct completion logic, prevent module loading for completions --- .../CommandCompletion/CompletionCompleters.cs | 26 +++++++++++----- .../engine/Modules/AnalysisCache.cs | 31 +++++++++++++++++++ .../engine/Modules/ModuleCmdletBase.cs | 3 +- .../engine/Modules/ModuleUtils.cs | 27 ++++++---------- ... => CompatiblePSEditions.Module.Tests.ps1} | 0 5 files changed, 61 insertions(+), 26 deletions(-) rename test/powershell/Modules/Microsoft.PowerShell.Core/{ModuleCompatiblePSEditions.Tests.ps1 => CompatiblePSEditions.Module.Tests.ps1} (100%) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index cf88b76bca7..86b513e64e3 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -398,7 +398,7 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun #region Module Names - internal static List CompleteModuleName(CompletionContext context, bool loadedModulesOnly) + internal static List CompleteModuleName(CompletionContext context, bool loadedModulesOnly, bool skipEditionCheck = false) { var moduleName = context.WordToComplete ?? string.Empty; var result = new List(); @@ -414,6 +414,10 @@ internal static List CompleteModuleName(CompletionContext cont { powershell.AddParameter("ListAvailable", true); } + if (skipEditionCheck) + { + powershell.AddParameter("SkipEditionCheck", true); + } Exception exceptionThrown; var psObjects = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); @@ -2101,17 +2105,19 @@ private static void NativeCommandArgumentCompletion( case "Get-Module": { bool loadedModulesOnly = boundArguments == null || !boundArguments.ContainsKey("ListAvailable"); - NativeCompletionModuleCommands(context, parameterName, loadedModulesOnly, /* isImportModule: */ false, result); + bool skipEditionCheck = boundArguments != null && 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); 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); break; } case "Debug-Process": @@ -3046,7 +3052,13 @@ private static void NativeCompletionScheduledJobCommands(CompletionContext conte } } - private static void NativeCompletionModuleCommands(CompletionContext context, string paramName, bool loadedModulesOnly, bool isImportModule, List result) + private static void NativeCompletionModuleCommands( + CompletionContext context, + string paramName, + List result, + bool loadedModulesOnly = false, + bool isImportModule = false, + bool skipEditionCheck = false) { if (string.IsNullOrEmpty(paramName)) { @@ -3067,7 +3079,7 @@ private static void NativeCompletionModuleCommands(CompletionContext context, st StringLiterals.PowerShellILAssemblyExtension, StringLiterals.PowerShellCmdletizationFileExtension }; - var moduleFilesResults = new List(CompleteFilename(context, /* containerOnly: */ false, moduleExtensions)); + var moduleFilesResults = new List(CompleteFilename(context, containerOnly: false, moduleExtensions)); if (moduleFilesResults.Count > 0) result.AddRange(moduleFilesResults); @@ -3079,7 +3091,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); diff --git a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs index 48e9fb15774..57f3f7b7dc4 100644 --- a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs +++ b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs @@ -100,6 +100,12 @@ private static ConcurrentDictionary AnalyzeManifestModule( var moduleManifestProperties = PsUtils.GetModuleManifestProperties(modulePath, PsUtils.FastModuleManifestAnalysisPropertyNames); if (moduleManifestProperties != null) { + if (ModuleIsEditionIncompatible(modulePath, moduleManifestProperties)) + { + ModuleIntrinsics.Tracer.WriteLine($"Module lies on edition-compatibility-checked path and is incompatible with current PowerShell edition, skipping module: {modulePath}"); + return null; + } + Version version; if (ModuleUtils.IsModuleInVersionSubdirectory(modulePath, out version)) { @@ -158,6 +164,31 @@ private static ConcurrentDictionary AnalyzeManifestModule( return result ?? AnalyzeTheOldWay(modulePath, context, lastWriteTime); } + /// + /// Check if a module is compatible with the current PSEdition given its path and its manifest properties. + /// + /// The path to the module. + /// The properties of the module's manifest. + /// + internal static bool ModuleIsEditionIncompatible(string modulePath, Hashtable moduleManifestProperties) + { +#if UNIX + return false; +#else + if (!ModuleUtils.ShouldCheckEditionOfModulesOnPath(modulePath)) + { + return false; + } + + if (!moduleManifestProperties.ContainsKey("CompatiblePSEditions")) + { + return true; + } + + return !Utils.IsPSEditionSupported(LanguagePrimitives.ConvertTo(moduleManifestProperties["CompatiblePSEditions"])); +#endif + } + internal static bool ModuleAnalysisViaGetModuleRequired(object modulePathObj, bool hadCmdlets, bool hadFunctions, bool hadAliases) { var modulePath = modulePathObj as string; diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 3bd000231bd..59034be684e 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -3403,8 +3403,7 @@ private bool IsPSEditionCompatible( return true; #else - string windowsPSModulePath = ModuleIntrinsics.GetWindowsPowerShellPSHomeModulePath(); - if (!moduleManifestPath.StartsWith(windowsPSModulePath, StringComparison.OrdinalIgnoreCase)) + if (!ModuleUtils.ShouldCheckEditionOfModulesOnPath(moduleManifestPath)) { return true; } diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 19e47f983e2..543041621c0 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -13,11 +13,6 @@ namespace System.Management.Automation.Internal { internal static class ModuleUtils { - // Cache for modules on the System32 module path, which are assumed to not be deleted or have their editions change. - // Null entries denote incompatible modules. - private static readonly ConcurrentDictionary s_incompatibleEditionSystem32Modules = - new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - // Default option for local file system enumeration: // - Ignore files/directories when access is denied; // - Search top directory only. @@ -351,6 +346,16 @@ internal static bool IsModuleInVersionSubdirectory(string modulePath, out Versio return false; } +#if !UNIX + internal static bool ShouldCheckEditionOfModulesOnPath(string path) + { + Dbg.Assert(!String.IsNullOrEmpty(path), $"Caller to verify that {nameof(path)} is not null or empty"); + + string windowsPowerShellPSHomePath = ModuleIntrinsics.GetWindowsPowerShellPSHomeModulePath(); + return path.StartsWith(windowsPowerShellPSHomePath, StringComparison.OrdinalIgnoreCase); + } +#endif + /// /// Gets a list of matching commands /// @@ -436,18 +441,6 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe string moduleShortName = System.IO.Path.GetFileNameWithoutExtension(modulePath); -// System32 module CompatiblePSEditions checks: -// incompatible modules should not appear as completions -#if !UNIX - // If the module is on the System32 path where CompatiblePSEditions are checked, - // we skip it -- only give completions for loaded modules on this path - string psCompatibleEditionsCheckedPath = ModuleIntrinsics.GetWindowsPowerShellPSHomeModulePath(); - if (modulePath.StartsWith(psCompatibleEditionsCheckedPath, StringComparison.OrdinalIgnoreCase)) - { - continue; - } -#endif - IDictionary exportedCommands = AnalysisCache.GetExportedCommands(modulePath, testOnly: false, context); if (exportedCommands == null) { continue; } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 similarity index 100% rename from test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 rename to test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 From d8779c675b3a556abfcdd58968a24ca55635002e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Jul 2018 17:33:49 -0700 Subject: [PATCH 04/35] [Feature] Use Enum.HasFlags, rename test file to execute after GAC test --- .../engine/Modules/ModuleCmdletBase.cs | 2 +- ...ns.Module.Tests.ps1 => ModuleCompatiblePSEditions.Tests.ps1} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/powershell/Modules/Microsoft.PowerShell.Core/{CompatiblePSEditions.Module.Tests.ps1 => ModuleCompatiblePSEditions.Tests.ps1} (100%) diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 59034be684e..1294a808a63 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2360,7 +2360,7 @@ internal PSModuleInfo LoadModuleManifest( { // If we're trying to load the module, return null so that caches // are not polluted - if ((manifestProcessingFlags & ManifestProcessingFlags.LoadElements) != 0) + if (manifestProcessingFlags.HasFlag(ManifestProcessingFlags.LoadElements)) { return null; } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 similarity index 100% rename from test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 rename to test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 From f7cba10508f90eb354c32e5516167860cc24f2d0 Mon Sep 17 00:00:00 2001 From: t-roholt Date: Thu, 5 Jul 2018 11:02:53 -0700 Subject: [PATCH 05/35] [Feature] Move disabled GAC test to new process --- .../Import-Module.Tests.ps1 | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 index 74ea9c373df..204b5320ed3 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 @@ -101,17 +101,14 @@ Describe "Import-Module with ScriptsToProcess" -Tags "CI" { Describe "Import-Module for Binary Modules in GAC" -Tags 'CI' { Context "Modules are not loaded from GAC" { - BeforeAll { - [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('DisableGACLoading', $true) - } - - AfterAll { - [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('DisableGACLoading', $false) - } - - It "Load PSScheduledJob from Windows Powershell Modules folder should fail" -Skip:(-not $IsWindows) { - $modulePath = Join-Path $env:windir "System32/WindowsPowershell/v1.0/Modules/PSScheduledJob" - { Import-Module $modulePath -SkipEditionCheck -ErrorAction SilentlyContinue } | Should -Throw -ErrorId 'FormatXmlUpdateException,Microsoft.PowerShell.Commands.ImportModuleCommand' + It "Load PSScheduledJob from Windows Powershell Modules folder should fail" -Skip:(-not $IsWindows) -Tags "Feature" { + $testScript = { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('DisableGACLoading', $true) + $modulePath = Join-Path $env:windir "System32/WindowsPowershell/v1.0/Modules/PSScheduledJob" + Import-Module $modulePath -SkipEditionCheck + } + $err = (& "$PSHOME\pwsh.exe" -c $testScript 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] })[0] + $err.FullyQualifiedErrorId | Should -Be "FormatXmlUpdateException,Microsoft.PowerShell.Commands.ImportModuleCommand" } } From eea20c30b451131024f120cd39e319db67f92eec Mon Sep 17 00:00:00 2001 From: t-roholt Date: Thu, 5 Jul 2018 11:31:59 -0700 Subject: [PATCH 06/35] [Feature] Tag GAC loading tests as 'Feature' --- .../Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 index 204b5320ed3..d64dcd0aca8 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 @@ -99,9 +99,9 @@ Describe "Import-Module with ScriptsToProcess" -Tags "CI" { } } -Describe "Import-Module for Binary Modules in GAC" -Tags 'CI' { +Describe "Import-Module for Binary Modules in GAC" -Tags 'Feature' { Context "Modules are not loaded from GAC" { - It "Load PSScheduledJob from Windows Powershell Modules folder should fail" -Skip:(-not $IsWindows) -Tags "Feature" { + It "Load PSScheduledJob from Windows Powershell Modules folder should fail" -Skip:(-not $IsWindows) { $testScript = { [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('DisableGACLoading', $true) $modulePath = Join-Path $env:windir "System32/WindowsPowershell/v1.0/Modules/PSScheduledJob" From f31cfcb5962fe81a20a3207cad47ab57daab1d1c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Jul 2018 09:48:44 -0700 Subject: [PATCH 07/35] [Feature] Address @daxian-dbw's comments --- .../engine/Modules/GetModuleCommand.cs | 3 +-- .../engine/Modules/ModuleCmdletBase.cs | 12 +----------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index 7d66bfad8e7..e6674b8c578 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -489,9 +489,8 @@ private IEnumerable FilterModulesForEditionAndSpecification( IEnumerable modules, IDictionary moduleSpecificationTable) { -// Edition check only applies to Windows -// System32 module path #if !UNIX + // Edition check only applies to Windows System32 module path if (!SkipEditionCheck) { modules = FilterModulesForIncompatibleOnEditionCheckedPath(modules); diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 1294a808a63..c100bec24c0 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2468,6 +2468,7 @@ internal PSModuleInfo LoadModuleManifest( manifestInfo.Copyright = copyright; manifestInfo.DotNetFrameworkVersion = requestedDotNetFrameworkVersion; manifestInfo.ClrVersion = requestedClrVersion; + manifestInfo.IsLoadedFromCompatibilityCheckedPath = isOnSystem32ModulePath; manifestInfo.PowerShellHostName = requestedHostName; manifestInfo.PowerShellHostVersion = requestedHostVersion; manifestInfo.PowerShellVersion = powerShellVersion; @@ -2502,12 +2503,6 @@ internal PSModuleInfo LoadModuleManifest( manifestInfo.AddToCompatiblePSEditions(compatiblePSEditions); } -// Modules loaded from the System32 module path may -// not have the CompatiblePSEditions fields ignored -#if !UNIX - manifestInfo.IsLoadedFromCompatibilityCheckedPath = isOnSystem32ModulePath; -#endif - if (scriptsToProcess != null) { foreach (var s in scriptsToProcess) @@ -3074,12 +3069,7 @@ internal PSModuleInfo LoadModuleManifest( newManifestInfo.LicenseUri = manifestInfo.LicenseUri; newManifestInfo.IconUri = manifestInfo.IconUri; newManifestInfo.RepositorySourceLocation = manifestInfo.RepositorySourceLocation; - -// On Windows, we need to copy over the field indicating -// whether the module was imported from the System32 module path -#if !UNIX newManifestInfo.IsLoadedFromCompatibilityCheckedPath = manifestInfo.IsLoadedFromCompatibilityCheckedPath; -#endif // If we are in module discovery, then fix the path. if (ss == null) From eba39497bd8f2b29e59decb88e5288c14d5d42ff Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 9 Jul 2018 10:59:28 -0700 Subject: [PATCH 08/35] [Feature] Add comment to explain new process for GAC loading test --- .../Import-Module.Tests.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 index d64dcd0aca8..263f5eacf4d 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 @@ -102,6 +102,18 @@ Describe "Import-Module with ScriptsToProcess" -Tags "CI" { Describe "Import-Module for Binary Modules in GAC" -Tags 'Feature' { Context "Modules are not loaded from GAC" { It "Load PSScheduledJob from Windows Powershell Modules folder should fail" -Skip:(-not $IsWindows) { + # NOTE: + # This test attempts to load an assembly from the GAC and expects to fail. + # However, if the assembly is already loaded, the assembly will be resolved before + # the DisableGACLoading flag comes into effect, meaning no error is thrown and the test fails. + # + # Since the System32 module direction is now on the module path and Get-Module -ListAvailable + # loads assemblies, the assembly is resolved and this test will fail if any other tests before + # it call Get-Module -ListAvailable. + # + # So for now, the solution is to run this test in a fresh process. + # Since disabling GAC loading is supposed to be a startup option, this is possibly appropriate. + $testScript = { [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('DisableGACLoading', $true) $modulePath = Join-Path $env:windir "System32/WindowsPowershell/v1.0/Modules/PSScheduledJob" From ee167a5105bc87ba9e29b7716eab4b6b74ee3aa9 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 9 Jul 2018 15:27:00 -0700 Subject: [PATCH 09/35] [Feature] Add test for PowerShell subprocesses --- .../CompatiblePSEditions.Tests.ps1 | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 new file mode 100644 index 00000000000..57d458b2bd4 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 @@ -0,0 +1,259 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$script:oldModulePath = $env:PSModulePath + +function Add-ModulePath +{ + param([string]$Path) + + $script:oldModulePath = $env:PSModulePath + + $env:PSModulePath = $env:PSModulePath + [System.IO.Path]::PathSeparator + $Path +} + +function Restore-ModulePath +{ + $env:PSModulePath = $script:oldModulePath +} + +# Creates a new dummy module compatible with the given PSEditions +function New-EditionCompatibleModule +{ + param( + [Parameter(Mandatory = $true)][string]$ModuleName, + [string]$DirPath, + [string[]]$CompatiblePSEditions) + + $modulePath = Join-Path $DirPath $ModuleName + + $manifestPath = Join-Path $modulePath "$ModuleName.psd1" + + $psm1Name = "$ModuleName.psm1" + $psm1Path = Join-Path $modulePath $psm1Name + + New-Item -Path $modulePath -ItemType Directory + + New-Item -Path $psm1Path -Value "function Test-$ModuleName { `$true }" + + if ($CompatiblePSEditions) + { + New-ModuleManifest -Path $manifestPath -CompatiblePSEditions $CompatiblePSEditions -RootModule $psm1Name + } + else + { + New-ModuleManifest -Path $manifestPath -RootModule $psm1Name + } + + return $modulePath +} + +function New-TestModules +{ + param([hashtable[]]$TestCases, [string]$BaseDir) + + for ($i = 0; $i -lt $TestCases.Count; $i++) + { + $path = New-EditionCompatibleModule -ModuleName $TestCases[$i].ModuleName -CompatiblePSEditions $TestCases[$i].Editions -Dir $BaseDir + + $TestCases[$i].Path = $path + $TestCases[$i].Name = $TestCases[$i].Editions -join "," + } +} + +Describe "Get-Module with CompatiblePSEditions-checked paths" -Tag "CI" { + + BeforeAll { + $successCases = @( + @{ Editions = "Core","Desktop"; ModuleName = "BothModule" }, + @{ Editions = "Core"; ModuleName = "CoreModule" } + ) + + $failCases = @( + @{ Editions = "Desktop"; ModuleName = "DesktopModule" }, + @{ Editions = $null; ModuleName = "NeitherModule" } + ) + + $basePath = Join-Path $TestDrive "EditionCompatibleModules" + New-TestModules -TestCases $successCases -BaseDir $basePath + New-TestModules -TestCases $failCases -BaseDir $basePath + + # Emulate the System32 module path for tests + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $basePath) + } + + AfterAll { + Remove-Item -Force -Recurse $basePath + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) + } + + Context "Loading from checked paths on the module path with no flags" { + BeforeAll { + Add-ModulePath $basePath + $modules = Get-Module -ListAvailable + } + + AfterAll { + Restore-ModulePath + } + + It "Lists compatible modules from the module path with -ListAvailable for PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules.Name | Should -Contain $ModuleName + } + + It "Does not list incompatible modules with -ListAvailable for PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules.Name | Should -Not -Contain $ModuleName + } + } + + Context "Loading from checked paths by absolute path with no flags" { + It "Lists compatible modules with -ListAvailable for PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules = Get-Module -ListAvailable (Join-Path -Path $basePath -ChildPath $ModuleName) + + $modules.Name | Should -Contain $ModuleName + } + + It "Does not list incompatible modules with -ListAvailable for PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules = Get-Module -ListAvailable (Join-Path -Path $basePath -ChildPath $ModuleName) + + $modules.Name | Should -Not -Contain $ModuleName + } + } + + Context "Loading from checked paths on the module path with -SkipEditionCheck" { + BeforeAll { + Add-ModulePath $basePath + $modules = Get-Module -ListAvailable -SkipEditionCheck + } + + AfterAll { + Restore-ModulePath + } + + It "Lists all modules from the module path with -ListAvailable for PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules.Name | Should -Contain $ModuleName + } + } + + Context "Loading from checked paths by absolute path with -SkipEditionCheck" { + It "Lists compatible modules with -ListAvailable for PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { + param($Editions, $ModuleName) + + $modules = Get-Module -ListAvailable -SkipEditionCheck (Join-Path -Path $basePath -ChildPath $ModuleName) + + $modules.Name | Should -Contain $ModuleName + } + } +} + +Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { + BeforeAll { + $successCases = @( + @{ Editions = "Core","Desktop"; ModuleName = "BothModule"; Result = $true }, + @{ Editions = "Core"; ModuleName = "CoreModule"; Result = $true } + ) + + $failCases = @( + @{ Editions = "Desktop"; ModuleName = "DesktopModule"; Result = $true }, + @{ Editions = $null; ModuleName = "NeitherModule"; Result = $true } + ) + + $basePath = Join-Path $TestDrive "EditionCompatibleModules" + New-TestModules -TestCases $successCases -BaseDir $basePath + New-TestModules -TestCases $failCases -BaseDir $basePath + + $allModules = ($successCases + $failCases).ModuleName + + # Emulate the System32 module path for tests + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $basePath) + } + + AfterAll { + Remove-Item -Force -Recurse $basePath + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) + } + + AfterEach { + Get-Module $allModules | Remove-Module -Force + } + + Context "Imports from module path" { + BeforeAll { + Add-ModulePath $basePath + } + + AfterAll { + Restore-ModulePath + } + + It "Successfully imports compatible modules from the module path with PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + Import-Module $ModuleName -Force + & "Test-$ModuleName" | Should -Be $Result + } + + It "Fails to import incompatible modules from the module path with PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + { Import-Module $ModuleName -Force -ErrorAction 'Stop'; & "Test-$ModuleName" } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Imports an incompatible module from the module path with -SkipEditionCheck with PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + Import-Module $ModuleName -SkipEditionCheck -Force + & "Test-$ModuleName" | Should -Be $Result + } + } + + Context "Imports from absolute path" { + It "Successfully imports compatible modules from an absolute path with PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + $path = Join-Path -Path $basePath -ChildPath $ModuleName + + Import-Module $path -Force + & "Test-$ModuleName" | Should -Be $Result + } + + It "Fails to import incompatible modules from an absolute path with PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + $path = Join-Path -Path $basePath -ChildPath $ModuleName + + { Import-Module $path -Force -ErrorAction 'Stop'; & "Test-$ModuleName" } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Imports an incompatible module from an absolute path with -SkipEditionCheck with PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { + param($Editions, $ModuleName, $Result) + + $path = Join-Path -Path $basePath -ChildPath $ModuleName + + Import-Module $path -SkipEditionCheck -Force + & "Test-$ModuleName" | Should -Be $Result + } + } +} + +Describe "PSModulePath changes interacting with other PowerShell processes" -Tag "Feature" { + It "Allows Windows PowerShell subprocesses to call `$PSHome modules still" { + $errors = powershell.exe -Command "Get-ChildItem" 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } + $errors | Should -Be $null + } + + It "Allows PowerShell Core 6 subprocesses to call core modules" { + $errors = pwsh.exe -Command "Get-ChildItem" 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } + $errors | Should -Be $null + } +} From 94394aa51f0c2313779de563629def1af387a265 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 9 Jul 2018 15:29:15 -0700 Subject: [PATCH 10/35] [Feature] Skip powershell.exe tests on non-windows --- .../Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 index 57d458b2bd4..cd9b74228aa 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 @@ -247,7 +247,7 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { } Describe "PSModulePath changes interacting with other PowerShell processes" -Tag "Feature" { - It "Allows Windows PowerShell subprocesses to call `$PSHome modules still" { + It "Allows Windows PowerShell subprocesses to call `$PSHome modules still" -Skip:(-not $IsWindows) { $errors = powershell.exe -Command "Get-ChildItem" 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } $errors | Should -Be $null } From ab702544c2802169736cabfc0ebae625ae18b9ac Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 11 Jul 2018 15:49:17 -0700 Subject: [PATCH 11/35] [Feature] Address feedback from @daxian-dbw * Define parameter set of -SkipEditionCheck for Get-Module * Prevent -SkipEditionCheck from being used without -ListAvailable with Get-Module * Prevent System32 modules from being cached if they are incompatible * Fix powershell subprocess tests * Remove IsLoadedFromCompatibilityCheckedPath from PSModuleInfo * Add IsCompatibleWithCurrentEdition to PSModuleInfo --- .../CommandCompletion/CompletionCompleters.cs | 12 +++++---- .../engine/Modules/AnalysisCache.cs | 12 +++++++-- .../engine/Modules/GetModuleCommand.cs | 21 +++++++++++++--- .../engine/Modules/ModuleCmdletBase.cs | 25 +++++++++---------- .../engine/Modules/ModuleIntrinsics.cs | 5 ++-- .../engine/Modules/ModuleUtils.cs | 2 +- .../engine/Modules/PSModuleInfo.cs | 7 +++--- .../CompatiblePSEditions.Tests.ps1 | 24 +++++++++++++++--- 8 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 86b513e64e3..9ef494da621 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -413,10 +413,12 @@ internal static List CompleteModuleName(CompletionContext cont if (!loadedModulesOnly) { powershell.AddParameter("ListAvailable", true); - } - if (skipEditionCheck) - { - powershell.AddParameter("SkipEditionCheck", true); + + // -SkipEditionCheck should only be set or apply to -ListAvailable + if (skipEditionCheck) + { + powershell.AddParameter("SkipEditionCheck", true); + } } Exception exceptionThrown; @@ -2105,7 +2107,7 @@ private static void NativeCommandArgumentCompletion( case "Get-Module": { bool loadedModulesOnly = boundArguments == null || !boundArguments.ContainsKey("ListAvailable"); - bool skipEditionCheck = boundArguments != null && boundArguments.ContainsKey("SkipEditionCheck"); + bool skipEditionCheck = !loadedModulesOnly && boundArguments.ContainsKey("SkipEditionCheck"); NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly, skipEditionCheck: skipEditionCheck); break; } diff --git a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs index 57f3f7b7dc4..2cfe0d8aea9 100644 --- a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs +++ b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs @@ -102,7 +102,7 @@ private static ConcurrentDictionary AnalyzeManifestModule( { if (ModuleIsEditionIncompatible(modulePath, moduleManifestProperties)) { - ModuleIntrinsics.Tracer.WriteLine($"Module lies on edition-compatibility-checked path and is incompatible with current PowerShell edition, skipping module: {modulePath}"); + ModuleIntrinsics.Tracer.WriteLine($"Module lies on the Windows System32 legacy module path and is incompatible with current PowerShell edition, skipping module: {modulePath}"); return null; } @@ -175,7 +175,7 @@ internal static bool ModuleIsEditionIncompatible(string modulePath, Hashtable mo #if UNIX return false; #else - if (!ModuleUtils.ShouldCheckEditionOfModulesOnPath(modulePath)) + if (!ModuleUtils.IsOnSystem32ModulePath(modulePath)) { return false; } @@ -478,6 +478,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.IsCompatibleWithCurrentEdition && ModuleUtils.IsOnSystem32ModulePath(module.ModuleBase)) + { + ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached."); + return; + } + DateTime lastWriteTime; ModuleCacheEntry moduleCacheEntry; GetModuleEntryFromCache(module.Path, out lastWriteTime, out moduleCacheEntry); diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index e6674b8c578..50d3da79045 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -87,7 +87,9 @@ public sealed class GetModuleCommand : ModuleCmdletBase, IDisposable /// /// When set, CompatiblePSEditions checking is disabled for modules in the System32 (Windows PowerShell) module directory. /// - [Parameter] + [Parameter(ParameterSetName = ParameterSet_AvailableLocally)] + [Parameter(ParameterSetName = ParameterSet_AvailableInPsrpSession)] + [Parameter(ParameterSetName = ParameterSet_AvailableInCimSession)] public SwitchParameter SkipEditionCheck { get { return (SwitchParameter)BaseSkipEditionCheck; } @@ -351,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("-SkipEditionCheck can only be used with -ListAvailable"), + "SkipEditionCheckCannotBeSpecifiedWithoutListAvailable", + ErrorCategory.InvalidOperation, + targetObject: null); + + ThrowTerminatingError(error); + } + var strNames = new List(); if (Name != null) { @@ -522,14 +536,13 @@ private IEnumerable FilterModulesForIncompatibleOnEditionCheckedPa foreach (PSModuleInfo module in modules) { - if (!module.IsLoadedFromCompatibilityCheckedPath) + if (!ModuleUtils.IsOnSystem32ModulePath(module.ModuleBase)) { yield return module; continue; } - IEnumerable moduleCompatibleEditions = module.CompatiblePSEditions.Any() ? module.CompatiblePSEditions : DefaultCompatiblePSEditions; - if (Utils.IsPSEditionSupported(moduleCompatibleEditions)) + if (module.IsCompatibleWithCurrentEdition) { yield return module; } diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index c100bec24c0..dfb741e8ae9 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2333,7 +2333,7 @@ internal PSModuleInfo LoadModuleManifest( // On Windows, we want to include any modules under %WINDIR%\System32\WindowsPowerShell\v1.0\Modules // that have declared compatibility with PS Core (or if the check is skipped) IEnumerable inferredCompatiblePSEditions = compatiblePSEditions ?? DefaultCompatiblePSEditions; - if (!IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions, out bool isOnSystem32ModulePath)) + if (!IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions, out bool doesModuleDeclareIsCompatible)) { containedErrors = true; @@ -2370,7 +2370,7 @@ internal PSModuleInfo LoadModuleManifest( return new PSModuleInfo(moduleManifestPath, context: null, sessionState: null) { HadErrorsLoading = true, - IsLoadedFromCompatibilityCheckedPath = isOnSystem32ModulePath + IsCompatibleWithCurrentEdition = false, }; } } @@ -2468,7 +2468,7 @@ internal PSModuleInfo LoadModuleManifest( manifestInfo.Copyright = copyright; manifestInfo.DotNetFrameworkVersion = requestedDotNetFrameworkVersion; manifestInfo.ClrVersion = requestedClrVersion; - manifestInfo.IsLoadedFromCompatibilityCheckedPath = isOnSystem32ModulePath; + manifestInfo.IsCompatibleWithCurrentEdition = doesModuleDeclareIsCompatible; manifestInfo.PowerShellHostName = requestedHostName; manifestInfo.PowerShellHostVersion = requestedHostVersion; manifestInfo.PowerShellVersion = powerShellVersion; @@ -3069,7 +3069,7 @@ internal PSModuleInfo LoadModuleManifest( newManifestInfo.LicenseUri = manifestInfo.LicenseUri; newManifestInfo.IconUri = manifestInfo.IconUri; newManifestInfo.RepositorySourceLocation = manifestInfo.RepositorySourceLocation; - newManifestInfo.IsLoadedFromCompatibilityCheckedPath = manifestInfo.IsLoadedFromCompatibilityCheckedPath; + newManifestInfo.IsCompatibleWithCurrentEdition = manifestInfo.IsCompatibleWithCurrentEdition; // If we are in module discovery, then fix the path. if (ss == null) @@ -3378,29 +3378,28 @@ internal PSModuleInfo LoadModuleManifest( /// /// The path to the module manifest being checked. /// The value of the CompatiblePSEditions field of the module manifest. - /// - /// True if the module is being loaded from the Windows PowerShell $PSHOME module path (under %WINDIR%\System32), false otherwise. + /// + /// True if the module itself declares it is compatible with the current edition, false otherwise. + /// This can be false and the method return true if the module did not come from the system32 path. /// /// True if the module is compatible with the running PowerShell edition, false otherwise. private bool IsPSEditionCompatible( string moduleManifestPath, IEnumerable compatiblePSEditions, - out bool isOnSystem32ModulePath) + out bool doesModuleDeclareIsCompatible) { - isOnSystem32ModulePath = false; - #if UNIX + doesModuleDeclareIsCompatible = true; return true; #else + doesModuleDeclareIsCompatible = Utils.IsPSEditionSupported(compatiblePSEditions); - if (!ModuleUtils.ShouldCheckEditionOfModulesOnPath(moduleManifestPath)) + if (!ModuleUtils.IsOnSystem32ModulePath(moduleManifestPath)) { return true; } - isOnSystem32ModulePath = true; - - return BaseSkipEditionCheck || Utils.IsPSEditionSupported(compatiblePSEditions); + return doesModuleDeclareIsCompatible || BaseSkipEditionCheck; #endif } diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index c3a49231a86..70e4f633dd7 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -916,10 +916,9 @@ private static string SetModulePath() if (!string.IsNullOrEmpty(newModulePathString)) { -// If on Windows, we want to add the System32 -// Windows PowerShell module directory -// so that Windows modules are discovered #if !UNIX + // If on Windows, we want to add the System32 Windows PowerShell module directory + // so that Windows modules are discoverable newModulePathString += Path.PathSeparator + GetWindowsPowerShellPSHomeModulePath(); #endif diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 543041621c0..a54f1911706 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -347,7 +347,7 @@ internal static bool IsModuleInVersionSubdirectory(string modulePath, out Versio } #if !UNIX - internal static bool ShouldCheckEditionOfModulesOnPath(string path) + internal static bool IsOnSystem32ModulePath(string path) { Dbg.Assert(!String.IsNullOrEmpty(path), $"Caller to verify that {nameof(path)} is not null or empty"); diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index 6be2ce94a50..c6d998678a6 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -847,11 +847,10 @@ internal void AddToCompatiblePSEditions(IEnumerable psEditions) } /// - /// Indicates whether this module has been loaded from a path where its compatibility should be checked. - /// If true, this module's CompatiblePSEditions should be checked against the current PowerShell Edition - /// before it is shown by cmdlets like Get-Module -ListAvailable. + /// Describes whether the module is compatible with the current PowerShell edition, + /// as determined by the module's CompatiblePSEditions field and the path is was loaded from. /// - internal bool IsLoadedFromCompatibilityCheckedPath { get; set; } = false; + internal bool IsCompatibleWithCurrentEdition { get; set; } /// /// ModuleList diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 index cd9b74228aa..fb9dea5f7cd 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 @@ -5,11 +5,18 @@ $script:oldModulePath = $env:PSModulePath function Add-ModulePath { - param([string]$Path) + param([string]$Path, [switch]$Prepend) $script:oldModulePath = $env:PSModulePath - $env:PSModulePath = $env:PSModulePath + [System.IO.Path]::PathSeparator + $Path + if ($Prepend) + { + $env:PSModulePAth = $Path + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + else + { + $env:PSModulePath = $env:PSModulePath + [System.IO.Path]::PathSeparator + $Path + } } function Restore-ModulePath @@ -83,7 +90,6 @@ Describe "Get-Module with CompatiblePSEditions-checked paths" -Tag "CI" { } AfterAll { - Remove-Item -Force -Recurse $basePath [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) } @@ -179,7 +185,6 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { } AfterAll { - Remove-Item -Force -Recurse $basePath [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) } @@ -247,6 +252,17 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { } Describe "PSModulePath changes interacting with other PowerShell processes" -Tag "Feature" { +<<<<<<< Updated upstream +======= + BeforeAll { + Add-ModulePath (Join-Path $env:windir "System32\WindowsPowerShell\v1.0\Modules") -Prepend + } + + AfterAll { + Restore-ModulePath + } + +>>>>>>> Stashed changes It "Allows Windows PowerShell subprocesses to call `$PSHome modules still" -Skip:(-not $IsWindows) { $errors = powershell.exe -Command "Get-ChildItem" 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } $errors | Should -Be $null From 986081790d9ffe3094038cd5e0772da375157bcf Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 11 Jul 2018 17:51:24 -0700 Subject: [PATCH 12/35] [Feature] Use PSModuleInfo.IsConsideredEditionCompatible --- .../engine/Modules/AnalysisCache.cs | 2 +- .../engine/Modules/GetModuleCommand.cs | 27 +--------------- .../engine/Modules/ModuleCmdletBase.cs | 32 ++++++++++++------- .../engine/Modules/PSModuleInfo.cs | 8 +++-- ... => CompatiblePSEditions.Module.Tests.ps1} | 3 -- 5 files changed, 27 insertions(+), 45 deletions(-) rename test/powershell/Modules/Microsoft.PowerShell.Core/{CompatiblePSEditions.Tests.ps1 => CompatiblePSEditions.Module.Tests.ps1} (99%) diff --git a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs index 2cfe0d8aea9..0df4eb99867 100644 --- a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs +++ b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs @@ -480,7 +480,7 @@ internal static void CacheModuleExports(PSModuleInfo module, ExecutionContext co // Don't cache incompatible modules on the system32 module path even if loaded with // -SkipEditionCheck, since it will break subsequent sessions. - if (!module.IsCompatibleWithCurrentEdition && ModuleUtils.IsOnSystem32ModulePath(module.ModuleBase)) + if (!module.IsConsideredEditionCompatible && ModuleUtils.IsOnSystem32ModulePath(module.ModuleBase)) { ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached."); return; diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index 50d3da79045..d37fb649366 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -507,7 +507,7 @@ private IEnumerable FilterModulesForEditionAndSpecification( // Edition check only applies to Windows System32 module path if (!SkipEditionCheck) { - modules = FilterModulesForIncompatibleOnEditionCheckedPath(modules); + modules = modules.Where(module => module.IsConsideredEditionCompatible); } #endif @@ -524,31 +524,6 @@ private IEnumerable FilterModulesForEditionAndSpecification( return modules; } - /// - /// Filter out all modules on the PowerShell-edition-checked path that are incompatible with - /// the current PowerShell edition from a given enumeration of modules. - /// - /// The modules to filter incompatible examples from. - /// All modules that are either not on the checked path or are compatible with the current PowerShell edition. - private IEnumerable FilterModulesForIncompatibleOnEditionCheckedPath(IEnumerable modules) - { - Dbg.Assert(!SkipEditionCheck, $"Caller to verify that {nameof(SkipEditionCheck)} is false"); - - foreach (PSModuleInfo module in modules) - { - if (!ModuleUtils.IsOnSystem32ModulePath(module.ModuleBase)) - { - yield return module; - continue; - } - - if (module.IsCompatibleWithCurrentEdition) - { - yield return module; - } - } - } - /// /// Filter an enumeration of modules based on what PowerShell editions they are compatible with. /// If the PSEdition parameter has been given, filter modules based on that. diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index dfb741e8ae9..fbe20053a6c 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2333,7 +2333,8 @@ internal PSModuleInfo LoadModuleManifest( // On Windows, we want to include any modules under %WINDIR%\System32\WindowsPowerShell\v1.0\Modules // that have declared compatibility with PS Core (or if the check is skipped) IEnumerable inferredCompatiblePSEditions = compatiblePSEditions ?? DefaultCompatiblePSEditions; - if (!IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions, out bool doesModuleDeclareIsCompatible)) + if (!IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions, + out bool declaresCoreCompatible, out bool isOnSystem32ModulePath)) { containedErrors = true; @@ -2370,7 +2371,7 @@ internal PSModuleInfo LoadModuleManifest( return new PSModuleInfo(moduleManifestPath, context: null, sessionState: null) { HadErrorsLoading = true, - IsCompatibleWithCurrentEdition = false, + IsConsideredEditionCompatible = false, }; } } @@ -2468,12 +2469,16 @@ internal PSModuleInfo LoadModuleManifest( manifestInfo.Copyright = copyright; manifestInfo.DotNetFrameworkVersion = requestedDotNetFrameworkVersion; manifestInfo.ClrVersion = requestedClrVersion; - manifestInfo.IsCompatibleWithCurrentEdition = doesModuleDeclareIsCompatible; manifestInfo.PowerShellHostName = requestedHostName; manifestInfo.PowerShellHostVersion = requestedHostVersion; manifestInfo.PowerShellVersion = powerShellVersion; manifestInfo.ProcessorArchitecture = requiredProcessorArchitecture; manifestInfo.Prefix = resolvedCommandPrefix; + + // A module is considered compatible if it's not on the System32 module path, or + // if it is and declared "Core" as a compatible PSEdition. + manifestInfo.IsConsideredEditionCompatible = declaresCoreCompatible || !isOnSystem32ModulePath; + if (assemblyList != null) { foreach (var a in assemblyList) @@ -3069,7 +3074,7 @@ internal PSModuleInfo LoadModuleManifest( newManifestInfo.LicenseUri = manifestInfo.LicenseUri; newManifestInfo.IconUri = manifestInfo.IconUri; newManifestInfo.RepositorySourceLocation = manifestInfo.RepositorySourceLocation; - newManifestInfo.IsCompatibleWithCurrentEdition = manifestInfo.IsCompatibleWithCurrentEdition; + newManifestInfo.IsConsideredEditionCompatible = manifestInfo.IsConsideredEditionCompatible; // If we are in module discovery, then fix the path. if (ss == null) @@ -3378,28 +3383,31 @@ internal PSModuleInfo LoadModuleManifest( /// /// The path to the module manifest being checked. /// The value of the CompatiblePSEditions field of the module manifest. - /// - /// True if the module itself declares it is compatible with the current edition, false otherwise. - /// This can be false and the method return true if the module did not come from the system32 path. + /// + /// True if the module explicitly declares compatiblity for the current PSEdition in its manifest, otherwise false. + /// + /// True if the module is on the system32 module path (where edition compatibility is checked), false otherwise. /// /// True if the module is compatible with the running PowerShell edition, false otherwise. private bool IsPSEditionCompatible( string moduleManifestPath, IEnumerable compatiblePSEditions, - out bool doesModuleDeclareIsCompatible) + out bool moduleDeclaresCoreCompatible, + out bool isOnSystem32ModulePath) { + moduleDeclaresCoreCompatible = Utils.IsPSEditionSupported(compatiblePSEditions); + isOnSystem32ModulePath = false; #if UNIX - doesModuleDeclareIsCompatible = true; return true; #else - doesModuleDeclareIsCompatible = Utils.IsPSEditionSupported(compatiblePSEditions); - if (!ModuleUtils.IsOnSystem32ModulePath(moduleManifestPath)) { return true; } - return doesModuleDeclareIsCompatible || BaseSkipEditionCheck; + isOnSystem32ModulePath = true; + + return BaseSkipEditionCheck || moduleDeclaresCoreCompatible; #endif } diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index c6d998678a6..6ef12c302ec 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -847,10 +847,12 @@ internal void AddToCompatiblePSEditions(IEnumerable psEditions) } /// - /// Describes whether the module is compatible with the current PowerShell edition, - /// as determined by the module's CompatiblePSEditions field and the path is was loaded from. + /// Describes whether the module was considered compatible at load time. + /// Any module not on the System32 module path should have this as true. + /// Modules loaded from the System32 module path will have this as true if they + /// have declared edition compatibility with PowerShell Core. /// - internal bool IsCompatibleWithCurrentEdition { get; set; } + internal bool IsConsideredEditionCompatible { get; set; } /// /// ModuleList diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 similarity index 99% rename from test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 rename to test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index fb9dea5f7cd..13a2e0ffebe 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -252,8 +252,6 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { } Describe "PSModulePath changes interacting with other PowerShell processes" -Tag "Feature" { -<<<<<<< Updated upstream -======= BeforeAll { Add-ModulePath (Join-Path $env:windir "System32\WindowsPowerShell\v1.0\Modules") -Prepend } @@ -262,7 +260,6 @@ Describe "PSModulePath changes interacting with other PowerShell processes" -Tag Restore-ModulePath } ->>>>>>> Stashed changes It "Allows Windows PowerShell subprocesses to call `$PSHome modules still" -Skip:(-not $IsWindows) { $errors = powershell.exe -Command "Get-ChildItem" 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } $errors | Should -Be $null From 03a239c7da7e544ec5da77a08e75fa0aefa1c8b5 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 11 Jul 2018 21:42:57 -0700 Subject: [PATCH 13/35] Make IsOnSystem32ModulePath() trivially supported on UNIX --- .../engine/Modules/ModuleUtils.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index a54f1911706..bb7772b5033 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -346,15 +346,17 @@ internal static bool IsModuleInVersionSubdirectory(string modulePath, out Versio return false; } -#if !UNIX internal static bool IsOnSystem32ModulePath(string path) { +#if UNIX + return true; +#else Dbg.Assert(!String.IsNullOrEmpty(path), $"Caller to verify that {nameof(path)} is not null or empty"); string windowsPowerShellPSHomePath = ModuleIntrinsics.GetWindowsPowerShellPSHomeModulePath(); return path.StartsWith(windowsPowerShellPSHomePath, StringComparison.OrdinalIgnoreCase); - } #endif + } /// /// Gets a list of matching commands From 0dc4be236193c5a2c9735d6c3cb74f6ac905e674 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 12 Jul 2018 09:40:35 -0700 Subject: [PATCH 14/35] [Feature] Use LINQ filter for Get-Module --- .../engine/Modules/GetModuleCommand.cs | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index d37fb649366..964efc50d2f 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -513,7 +513,7 @@ private IEnumerable FilterModulesForEditionAndSpecification( if (!String.IsNullOrEmpty(PSEdition)) { - modules = FilterModulesForEdition(modules); + modules = modules.Where(module => module.CompatiblePSEditions.Contains(PSEdition, StringComparer.OrdinalIgnoreCase)); } if (moduleSpecificationTable != null && moduleSpecificationTable.Count > 0) @@ -524,25 +524,6 @@ private IEnumerable FilterModulesForEditionAndSpecification( return modules; } - /// - /// Filter an enumeration of modules based on what PowerShell editions they are compatible with. - /// If the PSEdition parameter has been given, filter modules based on that. - /// - /// The modules to filter by edition. - /// All modules meeting the PSEdition constraint. - private IEnumerable FilterModulesForEdition(IEnumerable modules) - { - Dbg.Assert(!String.IsNullOrEmpty(PSEdition), $"Caller to verify that {nameof(PSEdition)} is not null or empty"); - - foreach (PSModuleInfo module in modules) - { - if (module.CompatiblePSEditions.Contains(PSEdition, StringComparer.OrdinalIgnoreCase)) - { - yield return module; - } - } - } - /// /// 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. From 78f33be5083a7e7930bb316915dab1d1ff2e34b5 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 12 Jul 2018 09:53:27 -0700 Subject: [PATCH 15/35] [Feature] Add comment explaining BaseSkipEditionCheck in Test-ModuleManifest --- .../engine/Modules/TestModuleManifestCommand.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs b/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs index b897b7c739f..e88a28f976c 100644 --- a/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs +++ b/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs @@ -24,10 +24,15 @@ namespace Microsoft.PowerShell.Commands public sealed class TestModuleManifestCommand : ModuleCmdletBase { /// - /// Creates an instance of the Test-ModuleManifest command + /// Creates an instance of the Test-ModuleManifest command. /// public TestModuleManifestCommand() { + // Test-ModuleManifest reads a manifest with ModuleCmdletBase.LoadModuleManifest(). + // This will error on an edition-incompatible manifest loaded from the System32 path, + // unless BaseSkipEditionCheck is true. Since Test-ModuleManifest shouldn't care about + // module edition (it just tests manifest validity), we always want to set this rather + // than provide it as a switch on the cmdlet. BaseSkipEditionCheck = true; } From 5ccab3c47f1dca178e73c38ffd053d95c341dc8d Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 12 Jul 2018 11:27:51 -0700 Subject: [PATCH 16/35] [Feature] Stop Get-Module -List -All from returning incompatible nested modules --- .../engine/Modules/ModuleCmdletBase.cs | 4 +- .../engine/Modules/ModuleUtils.cs | 73 ++++++++++++++++++- .../utils/PsUtils.cs | 12 ++- 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index fbe20053a6c..fdf67456c18 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -946,7 +946,7 @@ private IEnumerable GetModuleForRootedPaths(List modulePat } var availableModuleFiles = all - ? ModuleUtils.GetAllAvailableModuleFiles(resolvedModulePath) + ? ModuleUtils.GetAllAvailableModuleFiles(resolvedModulePath, BaseSkipEditionCheck) : ModuleUtils.GetModuleFilesFromAbsolutePath(resolvedModulePath); bool foundModule = false; @@ -1048,7 +1048,7 @@ private IEnumerable GetModulesFromOneModulePath(List names } IEnumerable moduleFiles = all - ? ModuleUtils.GetAllAvailableModuleFiles(modulePath) + ? ModuleUtils.GetAllAvailableModuleFiles(modulePath, BaseSkipEditionCheck) : ModuleUtils.GetDefaultAvailableModuleFiles(modulePath); foreach (string file in moduleFiles) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index bb7772b5033..b9b61d79f4c 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; @@ -68,9 +69,11 @@ internal static bool IsPossibleModuleDirectory(string dir) /// Get all module files by searching the given directory recursively. /// All sub-directories that could be a module folder will be searched. /// - internal static IEnumerable GetAllAvailableModuleFiles(string topDirectoryToCheck) + internal static IEnumerable GetAllAvailableModuleFiles(string topDirectoryToCheck, bool skipEditionCheck) { if (!Directory.Exists(topDirectoryToCheck)) { yield break; } + + if (IsIncompatibleSystem32ModuleDir(topDirectoryToCheck, skipEditionCheck)) { yield break; } var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions; Queue directoriesToCheck = new Queue(); @@ -84,7 +87,7 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto string[] subDirectories = Directory.GetDirectories(directoryToCheck, "*", options); foreach (string toAdd in subDirectories) { - if (IsPossibleModuleDirectory(toAdd)) + if (IsPossibleModuleDirectory(toAdd) && !IsIncompatibleSystem32ModuleDir(toAdd, skipEditionCheck)) { directoriesToCheck.Enqueue(toAdd); } @@ -108,6 +111,70 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto } } + internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath, bool skipEditionCheck) + { + // Skip the check means assume compatible + if (skipEditionCheck) + { + return false; + } + + // Not on System32 path means assume compatible + if (!ModuleUtils.IsOnSystem32ModulePath(directoryPath)) + { + return false; + } + + Hashtable manifest; + try + { + string dirName = Path.GetFileName(directoryPath.TrimEnd('/', '\\')); + string manifestPath = Path.Join(directoryPath, dirName + ".psd1"); + if (!File.Exists(manifestPath)) + { + // If there's no manifest, it might be an ordinary directory, so not incompatible + return false; + } + + manifest = PsUtils.GetModuleManifestProperties(manifestPath, new [] { "CompatiblePSEditions" }); + } + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) + { + // Take the safe option if we hit an exception + return false; + } + + object psEditionsObj = manifest?["CompatiblePSEditions"]; + if (psEditionsObj == null) + { + // If there is a manifest file but not "CompatiblePSEditions" key, + // the module is incompatible + return true; + } + + string[] psEditions; + try + { + psEditions = LanguagePrimitives.ConvertTo(psEditionsObj); + } + catch (PSInvalidCastException) + { + // If the "CompatiblePSEditions" key exists but isn't a string[], + // the module is bad + return true; + } + + if (psEditions == null) + { + // If somehow the key was set but set as $null, that's the same as not setting it + // Meaning incompatible + return true; + } + + // Finally check the edition flags of the module + return Utils.IsPSEditionSupported(psEditions); + } + internal static IEnumerable GetDefaultAvailableModuleFiles(bool isForAutoDiscovery, ExecutionContext context) { HashSet uniqueModuleFiles = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -349,7 +416,7 @@ internal static bool IsModuleInVersionSubdirectory(string modulePath, out Versio internal static bool IsOnSystem32ModulePath(string path) { #if UNIX - return true; + return false; #else Dbg.Assert(!String.IsNullOrEmpty(path), $"Caller to verify that {nameof(path)} is not null or empty"); diff --git a/src/System.Management.Automation/utils/PsUtils.cs b/src/System.Management.Automation/utils/PsUtils.cs index 3fe10bacba2..a542db603f3 100644 --- a/src/System.Management.Automation/utils/PsUtils.cs +++ b/src/System.Management.Automation/utils/PsUtils.cs @@ -476,7 +476,17 @@ internal static Hashtable EvaluatePowerShellDataFile( internal static readonly string[] ManifestModuleVersionPropertyName = new[] { "ModuleVersion" }; internal static readonly string[] ManifestGuidPropertyName = new[] { "GUID" }; - internal static readonly string[] FastModuleManifestAnalysisPropertyNames = new[] { "AliasesToExport", "CmdletsToExport", "FunctionsToExport", "NestedModules", "RootModule", "ModuleToProcess", "ModuleVersion" }; + internal static readonly string[] FastModuleManifestAnalysisPropertyNames = new[] + { + "AliasesToExport", + "CmdletsToExport", + "CompatiblePSEditions", + "FunctionsToExport", + "NestedModules", + "RootModule", + "ModuleToProcess", + "ModuleVersion" + }; internal static Hashtable GetModuleManifestProperties(string psDataFilePath, string[] keys) { From ae6501a92ceaa943cb425fe6e22134bd84516558 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 12 Jul 2018 13:46:31 -0700 Subject: [PATCH 17/35] [Feature] Add comment explaining module file filtering --- .../engine/Modules/ModuleUtils.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index b9b61d79f4c..84a8607c385 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -111,6 +111,16 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto } } + /// + /// Checks a directory to see if it's the root of a Core-incompatible module on the + /// System32 module path. + /// + /// The path of the directory to check. + /// If true, skips the check and returns false. + /// + /// If the directory is the root of a module on the System32 path and not marked as + /// Core-compatible, returns true, otherwise returns false. + /// internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath, bool skipEditionCheck) { // Skip the check means assume compatible From 02b19d03e1b8a0abc64ac813a63c5090e0d96f52 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 12 Jul 2018 22:02:41 -0700 Subject: [PATCH 18/35] [Feature] Fix compatibility logic, add Get-Module -List -All tests --- .../engine/Modules/ModuleUtils.cs | 2 +- .../engine/Modules/PSModuleInfo.cs | 2 +- .../CompatiblePSEditions.Module.Tests.ps1 | 235 ++++++++++++++++++ 3 files changed, 237 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 84a8607c385..1decef12a5b 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -182,7 +182,7 @@ internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath, bool } // Finally check the edition flags of the module - return Utils.IsPSEditionSupported(psEditions); + return !Utils.IsPSEditionSupported(psEditions); } internal static IEnumerable GetDefaultAvailableModuleFiles(bool isForAutoDiscovery, ExecutionContext context) diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index 6ef12c302ec..74750cc83cc 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -852,7 +852,7 @@ internal void AddToCompatiblePSEditions(IEnumerable psEditions) /// Modules loaded from the System32 module path will have this as true if they /// have declared edition compatibility with PowerShell Core. /// - internal bool IsConsideredEditionCompatible { get; set; } + internal bool IsConsideredEditionCompatible { get; set; } = true; /// /// ModuleList diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 13a2e0ffebe..a02a4890229 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -270,3 +270,238 @@ Describe "PSModulePath changes interacting with other PowerShell processes" -Tag $errors | Should -Be $null } } + +Describe "Nested module behaviour" -Tag "Feature" { + BeforeAll { + $testConditions = @{ + SkipEditionCheck = @($true, $false) + UseRootModule = @($true, $false) + UseAbsolutePath = @($true, $false) + MarkedEdition = @($null, "Desktop", "Core", @("Desktop","Core")) + } + + # Combine all the test conditions into a list of test cases + $testCases = @(@{}) + foreach ($condition in $testConditions.Keys) + { + $list = [System.Collections.Generic.List[hashtable]]::new() + foreach ($obj in $testCases) + { + foreach ($value in $testConditions[$condition]) + { + $list.Add($obj + @{ $condition = $value }) + } + } + $testCases = $list + } + + # Define nested script module + $scriptModuleName = "NestedScriptModule" + $scriptModuleFile = "$scriptModuleName.psm1" + $scriptModuleContent = 'function Test-ScriptModule { return $true }' + + # Define nested binary module + $binaryModuleName = "NestedBinaryModule" + $binaryModuleFile = "$binaryModuleName.dll" + $binaryModuleContent = 'public static class TestBinaryModuleClass { public static bool Test() { return true; } }' + $binaryModuleSourcePath = Join-Path $TestDrive $binaryModuleFile + Add-Type -OutputAssembly $binaryModuleSourcePath -TypeDefinition $binaryModuleContent + + # Define root module definition + $rootModuleName = "RootModule" + $rootModuleFile = "$rootModuleName.psm1" + $rootModuleContent = 'function Test-RootModule { Test-ScriptModule; [TestBinaryModuleClass]::Test() }' + + # Module directory structure: $TestDrive/$compatibility/$guid/$moduleName/{module parts} + $compatibleDir = "Compatible" + $incompatibleDir = "Incompatible" + $compatiblePath = Join-Path $TestDrive $compatibleDir + $incompatiblePath = Join-Path $TestDrive $incompatibleDir + + foreach ($basePath in $compatiblePath,$incompatiblePath) + { + New-Item -Path $basePath -ItemType Directory + } + + # Set up the test state + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $incompatiblePath) + } + + AfterAll { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) + } + + Context "Get-Module -ListAvailable -All results OFF the System32 path" { + BeforeEach { + # Create the module directory + $guid = New-Guid + $compatibilityDir = $compatibleDir + $containingDir = Join-Path $TestDrive $compatibilityDir $guid + $moduleBase = Join-Path $containingDir "CpseTestModule" + New-Item -Path $moduleBase -ItemType Directory + Add-ModulePath $containingDir + } + + AfterEach { + Restore-ModulePath + } + + It "Gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows){ + param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) + + # Add nested module bits + $nestedModules = [System.Collections.ArrayList]::new() + # Script module + New-Item -Path (Join-Path $moduleBase $scriptModuleFile) -Value $scriptModuleContent + # Binary module + Copy-Item -Path $binaryModuleSourcePath -Destination (Join-Path $moduleBase $binaryModuleFile) + # Root module + if ($UseRootModule) + { + New-Item -Path (Join-Path $moduleBase $rootModuleFile) -Value $rootModuleContent + } + + $nestedModules = $scriptModuleFile,$binaryModuleFile + $nestedModuleNames = $nestedModules -join ',' + + # Create the manifest + $moduleName = Split-Path -Leaf $moduleBase + $manifestPath = Join-Path $moduleBase "$moduleName.psd1" + $compatibleEditions = if ($CompatiblePSEditions) { $CompatiblePSEditions -join "," } + + $newManifestCmd = "New-ModuleManifest -Path $manifestPath -NestedModules $nestedModuleNames " + if ($compatibleEditions) { $newManifestCmd += "-CompatiblePSEditions $compatibleEditions " } + if ($UseRootModule) { $newManifestCmd += "-RootModule $rootModuleFile " } + [scriptblock]::Create($newManifestCmd).Invoke() + + # Modules specified with an absolute path should only return themselves + if ($UseAbsolutePath) + { + $modules = Get-Module -ListAvailable -All $moduleBase + + $modules.Count | Should -Be 1 + $modules[0].Name | Should -BeExactly $moduleName + return + } + + $modules = if ($SkipEditionCheck) + { + Get-Module -ListAvailable -All -SkipEditionCheck | Where-Object { $_.Path.Contains($guid) } + } + else + { + Get-Module -ListAvailable -All | Where-Object { $_.Path.Contains($guid) } + } + + if ($UseRootModule) + { + $modules.Count | Should -Be 4 + } + else + { + $modules.Count | Should -Be 3 + } + + $names = $modules.Name + $names | Should -Contain $moduleName + $names | Should -Contain $scriptModuleName + $names | Should -Contain $binaryModuleName + } + } + + Context "Get-Module -ListAvailable -All results ON the System32 path" { + BeforeEach { + # Create the module directory + $guid = New-Guid + $compatibilityDir = $incompatibleDir + $containingDir = Join-Path $TestDrive $compatibilityDir $guid + $moduleBase = Join-Path $containingDir "CpseTestModule" + New-Item -Path $moduleBase -ItemType Directory + Add-ModulePath $containingDir + } + + AfterEach { + Restore-ModulePath + } + + It "Gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows){ + param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) + + # Add nested module bits + $nestedModules = [System.Collections.ArrayList]::new() + # Script module + New-Item -Path (Join-Path $moduleBase $scriptModuleFile) -Value $scriptModuleContent + # Binary module + Copy-Item -Path $binaryModuleSourcePath -Destination (Join-Path $moduleBase $binaryModuleFile) + # Root module + if ($UseRootModule) + { + New-Item -Path (Join-Path $moduleBase $rootModuleFile) -Value $rootModuleContent + } + + $nestedModules = $scriptModuleFile,$binaryModuleFile + $nestedModuleNames = $nestedModules -join ',' + + # Create the manifest + $moduleName = Split-Path -Leaf $moduleBase + $manifestPath = Join-Path $moduleBase "$moduleName.psd1" + $compatibleEditions = if ($MarkedEdition) { $MarkedEdition -join "," } + + $newManifestCmd = "New-ModuleManifest -Path $manifestPath -NestedModules $nestedModuleNames " + if ($compatibleEditions) { $newManifestCmd += "-CompatiblePSEditions $compatibleEditions " } + if ($UseRootModule) { $newManifestCmd += "-RootModule $rootModuleFile " } + [scriptblock]::Create($newManifestCmd).Invoke() + + # Modules specified with an absolute path should only return themselves + if ($UseAbsolutePath) { + if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) + { + { Get-Module -ListAvailable -All $moduleBase } | Should -Throw -ErrorId "Modules_ModuleNotFoundForGetModule,Microsoft.PowerShell.Commands.GetModuleCommand" + return + } + + $modules = if ($SkipEditionCheck) + { + Get-Module -ListAvailable -All -SkipEditionCheck $moduleBase + } + else + { + Get-Module -ListAvailable -All $moduleBase + } + + $modules.Count | Should -Be 1 + $modules[0].Name | Should -BeExactly $moduleName + return + } + + $modules = if ($SkipEditionCheck) + { + Get-Module -ListAvailable -All -SkipEditionCheck | Where-Object { $_.Path.Contains($guid) } + } + else + { + Get-Module -ListAvailable -All | Where-Object { $_.Path.Contains($guid) } + } + + if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) + { + $modules.Count | Should -Be 0 + return + } + + if ($UseRootModule) + { + $modules.Count | Should -Be 4 + } + else + { + $modules.Count | Should -Be 3 + } + + $names = $modules.Name + $names | Should -Contain $moduleName + $names | Should -Contain $scriptModuleName + $names | Should -Contain $binaryModuleName + } + } +} From 95f2c8e192a6dc5c1d16af4bfc43d9b17d1ec56a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 09:30:12 -0700 Subject: [PATCH 19/35] [Feature] Add -ErrorAction Stop to Get-Module tests for correct behaviour --- .../CompatiblePSEditions.Module.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index a02a4890229..2e22a953460 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -456,7 +456,7 @@ Describe "Nested module behaviour" -Tag "Feature" { if ($UseAbsolutePath) { if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) { - { Get-Module -ListAvailable -All $moduleBase } | Should -Throw -ErrorId "Modules_ModuleNotFoundForGetModule,Microsoft.PowerShell.Commands.GetModuleCommand" + { Get-Module -ListAvailable -All $moduleBase -ErrorAction Stop } | Should -Throw -ErrorId "Modules_ModuleNotFoundForGetModule,Microsoft.PowerShell.Commands.GetModuleCommand" return } From e70850734c7ce8f50a61d9dc624d29154ddbfd8e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 09:44:26 -0700 Subject: [PATCH 20/35] [Feature] Skip powershell subprocess tests on UNIX --- .../CompatiblePSEditions.Module.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 2e22a953460..f0a3d5c0dc3 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -265,7 +265,7 @@ Describe "PSModulePath changes interacting with other PowerShell processes" -Tag $errors | Should -Be $null } - It "Allows PowerShell Core 6 subprocesses to call core modules" { + It "Allows PowerShell Core 6 subprocesses to call core modules" -Skip:(-not $IsWindows) { $errors = pwsh.exe -Command "Get-ChildItem" 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } $errors | Should -Be $null } From 8f835d03d0566ded4b329df9f7f09ea4688ce1bb Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 09:46:48 -0700 Subject: [PATCH 21/35] [Feature] Skip BeforeAll blocks of Windows-only describes --- .../CompatiblePSEditions.Module.Tests.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index f0a3d5c0dc3..1a0b1c1d86d 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -71,6 +71,11 @@ function New-TestModules Describe "Get-Module with CompatiblePSEditions-checked paths" -Tag "CI" { BeforeAll { + if (-not $IsWindows) + { + return + } + $successCases = @( @{ Editions = "Core","Desktop"; ModuleName = "BothModule" }, @{ Editions = "Core"; ModuleName = "CoreModule" } @@ -273,6 +278,11 @@ Describe "PSModulePath changes interacting with other PowerShell processes" -Tag Describe "Nested module behaviour" -Tag "Feature" { BeforeAll { + if (-not $IsWindows) + { + return + } + $testConditions = @{ SkipEditionCheck = @($true, $false) UseRootModule = @($true, $false) From fc7d33b3a5fc0761410289bdb2fc820d828e7e02 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 13:06:21 -0700 Subject: [PATCH 22/35] [Feature] Remove duplicate test file, add more granular test for Get-WinEvent --- .../CompatiblePSEditions.Module.Tests.ps1 | 17 +- .../ModuleCompatiblePSEditions.Tests.ps1 | 249 ------------------ .../Get-WinEvent.Tests.ps1 | 15 ++ 3 files changed, 24 insertions(+), 257 deletions(-) delete mode 100644 test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 1a0b1c1d86d..4120d01e3a4 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -332,16 +332,17 @@ Describe "Nested module behaviour" -Tag "Feature" { { New-Item -Path $basePath -ItemType Directory } - - # Set up the test state - [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $incompatiblePath) } - AfterAll { - [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) - } + Context "Modules ON the System32 module path" { + BeforeAll { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $incompatiblePath) + } + + AfterAll { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) + } - Context "Get-Module -ListAvailable -All results OFF the System32 path" { BeforeEach { # Create the module directory $guid = New-Guid @@ -419,7 +420,7 @@ Describe "Nested module behaviour" -Tag "Feature" { } } - Context "Get-Module -ListAvailable -All results ON the System32 path" { + Context "Modules OFF the System32 test path" { BeforeEach { # Create the module directory $guid = New-Guid diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 deleted file mode 100644 index dc4c4225115..00000000000 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/ModuleCompatiblePSEditions.Tests.ps1 +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# NOTE: This test suite must be named/located so that it comes after the -# "Disable GAC loading" test in Import-Module.Tests.ps1. This is because that -# test will fail when the type it's trying to load is already in the cache and succeeds. - -$script:oldModulePath = $env:PSModulePath - -function Add-ModulePath -{ - param([string]$Path) - - $script:oldModulePath = $env:PSModulePath - - $env:PSModulePath = $env:PSModulePath + [System.IO.Path]::PathSeparator + $Path -} - -function Restore-ModulePath -{ - $env:PSModulePath = $script:oldModulePath -} - -# Creates a new dummy module compatible with the given PSEditions -function New-EditionCompatibleModule -{ - param( - [Parameter(Mandatory = $true)][string]$ModuleName, - [string]$DirPath, - [string[]]$CompatiblePSEditions) - - $modulePath = Join-Path $DirPath $ModuleName - - $manifestPath = Join-Path $modulePath "$ModuleName.psd1" - - $psm1Name = "$ModuleName.psm1" - $psm1Path = Join-Path $modulePath $psm1Name - - New-Item -Path $modulePath -ItemType Directory - - New-Item -Path $psm1Path -Value "function Test-$ModuleName { `$true }" - - if ($CompatiblePSEditions) - { - New-ModuleManifest -Path $manifestPath -CompatiblePSEditions $CompatiblePSEditions -RootModule $psm1Name - } - else - { - New-ModuleManifest -Path $manifestPath -RootModule $psm1Name - } - - return $modulePath -} - -function New-TestModules -{ - param([hashtable[]]$TestCases, [string]$BaseDir) - - for ($i = 0; $i -lt $TestCases.Count; $i++) - { - $path = New-EditionCompatibleModule -ModuleName $TestCases[$i].ModuleName -CompatiblePSEditions $TestCases[$i].Editions -Dir $BaseDir - - $TestCases[$i].Path = $path - $TestCases[$i].Name = $TestCases[$i].Editions -join "," - } -} - -Describe "Get-Module with CompatiblePSEditions-checked paths" -Tag "CI" { - - BeforeAll { - $successCases = @( - @{ Editions = "Core","Desktop"; ModuleName = "BothModule" }, - @{ Editions = "Core"; ModuleName = "CoreModule" } - ) - - $failCases = @( - @{ Editions = "Desktop"; ModuleName = "DesktopModule" }, - @{ Editions = $null; ModuleName = "NeitherModule" } - ) - - $basePath = Join-Path $TestDrive "EditionCompatibleModules" - New-TestModules -TestCases $successCases -BaseDir $basePath - New-TestModules -TestCases $failCases -BaseDir $basePath - - # Emulate the System32 module path for tests - [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $basePath) - } - - AfterAll { - [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) - } - - Context "Loading from checked paths on the module path with no flags" { - BeforeAll { - Add-ModulePath $basePath - $modules = Get-Module -ListAvailable - } - - AfterAll { - Restore-ModulePath - } - - It "Lists compatible modules from the module path with -ListAvailable for PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { - param($Editions, $ModuleName) - - $modules.Name | Should -Contain $ModuleName - } - - It "Does not list incompatible modules with -ListAvailable for PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { - param($Editions, $ModuleName) - - $modules.Name | Should -Not -Contain $ModuleName - } - } - - Context "Loading from checked paths by absolute path with no flags" { - It "Lists compatible modules with -ListAvailable for PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { - param($Editions, $ModuleName) - - $modules = Get-Module -ListAvailable (Join-Path -Path $basePath -ChildPath $ModuleName) - - $modules.Name | Should -Contain $ModuleName - } - - It "Does not list incompatible modules with -ListAvailable for PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { - param($Editions, $ModuleName) - - $modules = Get-Module -ListAvailable (Join-Path -Path $basePath -ChildPath $ModuleName) - - $modules.Name | Should -Not -Contain $ModuleName - } - } - - Context "Loading from checked paths on the module path with -SkipEditionCheck" { - BeforeAll { - Add-ModulePath $basePath - $modules = Get-Module -ListAvailable -SkipEditionCheck - } - - AfterAll { - Restore-ModulePath - } - - It "Lists all modules from the module path with -ListAvailable for PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { - param($Editions, $ModuleName) - - $modules.Name | Should -Contain $ModuleName - } - } - - Context "Loading from checked paths by absolute path with -SkipEditionCheck" { - It "Lists compatible modules with -ListAvailable for PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { - param($Editions, $ModuleName) - - $modules = Get-Module -ListAvailable -SkipEditionCheck (Join-Path -Path $basePath -ChildPath $ModuleName) - - $modules.Name | Should -Contain $ModuleName - } - } -} - -Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { - BeforeAll { - $successCases = @( - @{ Editions = "Core","Desktop"; ModuleName = "BothModule"; Result = $true }, - @{ Editions = "Core"; ModuleName = "CoreModule"; Result = $true } - ) - - $failCases = @( - @{ Editions = "Desktop"; ModuleName = "DesktopModule"; Result = $true }, - @{ Editions = $null; ModuleName = "NeitherModule"; Result = $true } - ) - - $basePath = Join-Path $TestDrive "EditionCompatibleModules" - New-TestModules -TestCases $successCases -BaseDir $basePath - New-TestModules -TestCases $failCases -BaseDir $basePath - - $allModules = ($successCases + $failCases).ModuleName - - # Emulate the System32 module path for tests - [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $basePath) - } - - AfterAll { - [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) - } - - AfterEach { - Get-Module $allModules | Remove-Module -Force - } - - Context "Imports from module path" { - BeforeAll { - Add-ModulePath $basePath - } - - AfterAll { - Restore-ModulePath - } - - It "Successfully imports compatible modules from the module path with PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { - param($Editions, $ModuleName, $Result) - - Import-Module $ModuleName -Force - & "Test-$ModuleName" | Should -Be $Result - } - - It "Fails to import incompatible modules from the module path with PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { - param($Editions, $ModuleName, $Result) - - { Import-Module $ModuleName -Force -ErrorAction 'Stop'; & "Test-$ModuleName" } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" - } - - It "Imports an incompatible module from the module path with -SkipEditionCheck with PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { - param($Editions, $ModuleName, $Result) - - Import-Module $ModuleName -SkipEditionCheck -Force - & "Test-$ModuleName" | Should -Be $Result - } - } - - Context "Imports from absolute path" { - It "Successfully imports compatible modules from an absolute path with PSEdition " -TestCases $successCases -Skip:(-not $IsWindows) { - param($Editions, $ModuleName, $Result) - - $path = Join-Path -Path $basePath -ChildPath $ModuleName - - Import-Module $path -Force - & "Test-$ModuleName" | Should -Be $Result - } - - It "Fails to import incompatible modules from an absolute path with PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { - param($Editions, $ModuleName, $Result) - - $path = Join-Path -Path $basePath -ChildPath $ModuleName - - { Import-Module $path -Force -ErrorAction 'Stop'; & "Test-$ModuleName" } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" - } - - It "Imports an incompatible module from an absolute path with -SkipEditionCheck with PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { - param($Editions, $ModuleName, $Result) - - $path = Join-Path -Path $basePath -ChildPath $ModuleName - - Import-Module $path -SkipEditionCheck -Force - & "Test-$ModuleName" | Should -Be $Result - } - } -} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Diagnostics/Get-WinEvent.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Diagnostics/Get-WinEvent.Tests.ps1 index f5b5929bda5..4cbc77a2938 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Diagnostics/Get-WinEvent.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Diagnostics/Get-WinEvent.Tests.ps1 @@ -54,6 +54,21 @@ Describe 'Get-WinEvent' -Tags "CI" { It 'Get-WinEvent can use the simplest of filters' { $filter = @{ ProviderName = $providerForTests.Name } $testEvents = Get-WinEvent -filterhashtable $filter + + $testEventDict = [System.Collections.Generic.Dictionary[int, System.Diagnostics.Eventing.Reader.EventLogRecord]]::new() + foreach ($te in $testEvents) + { + $testEventDict.TryAdd($te.Id, $te) + } + + foreach ($e in $events) + { + if (-not $testEventDict.ContainsKey($e.Id)) + { + throw new "Unexpected event log: $e" + } + } + $testEvents.Count | Should -Be $events.Count } It 'Get-WinEvent can use a filter which includes two items' { From d9b0f9defdbfd66aed5f3cbb55b1bf3ca77843a4 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 14:19:08 -0700 Subject: [PATCH 23/35] [Feature] Factor out module setup into functions --- .../CompatiblePSEditions.Module.Tests.ps1 | 185 ++++++++++-------- 1 file changed, 105 insertions(+), 80 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 4120d01e3a4..43a159a57cd 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -68,6 +68,55 @@ function New-TestModules } } +function New-TestNestedModule +{ + param( + [string]$ModuleBase, + [string]$ScriptModuleFilename, + [string]$ScriptModuleContent, + [string]$BinaryModuleFilename, + [string]$BinaryModuleDllPath, + [string]$RootModuleFilename, + [string]$RootModuleContent, + [string[]]$CompatiblePSEditions, + [bool]$UseRootModule, + [bool]$UseAbsolutePath + ) + + # Create script module + New-Item -Path (Join-Path $ModuleBase $ScriptModuleFileName) -Value $ScriptModuleContent + + # Create binary module + Copy-Item -Path $BinaryModuleDllPath -Destination (Join-Path $ModuleBase $BinaryModuleFilename) + + # Create the root module if there is one + if ($UseRootModule) + { + New-Item -Path (Join-Path $ModuleBase $RootModuleFilename) -Value $RootModuleContent + } + + # Create the manifest command + $moduleName = Split-Path -Leaf $ModuleBase + $manifestPath = Join-Path $ModuleBase "$moduleName.psd1" + + $nestedModules = $ScriptModuleFilename,$BinaryModuleFilename -join ',' + + $newManifestCmd = "New-ModuleManifest -Path $manifestPath -NestedModules $nestedModules " + if ($CompatiblePSEditions) + { + $compatibleModules = $CompatiblePSEditions -join ',' + $newManifestCmd += "-CompatiblePSEditions $compatibleModules " + } + if ($UseRootModule) + { + $newManifestCmd += "-RootModule $RootModuleFilename " + } + + # Create the manifest + [scriptblock]::Create($newManifestCmd).Invoke() +} + + Describe "Get-Module with CompatiblePSEditions-checked paths" -Tag "CI" { BeforeAll { @@ -334,7 +383,7 @@ Describe "Nested module behaviour" -Tag "Feature" { } } - Context "Modules ON the System32 module path" { + Context "Modules OFF the System32 test path" { BeforeAll { [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $incompatiblePath) } @@ -346,9 +395,10 @@ Describe "Nested module behaviour" -Tag "Feature" { BeforeEach { # Create the module directory $guid = New-Guid - $compatibilityDir = $compatibleDir + $compatibilityDir = $incompatibleDir $containingDir = Join-Path $TestDrive $compatibilityDir $guid - $moduleBase = Join-Path $containingDir "CpseTestModule" + $moduleName = "CpseTestModule" + $moduleBase = Join-Path $containingDir $moduleName New-Item -Path $moduleBase -ItemType Directory Add-ModulePath $containingDir } @@ -357,38 +407,37 @@ Describe "Nested module behaviour" -Tag "Feature" { Restore-ModulePath } - It "Gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows){ + It "Get-Module -ListAvailable -All gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows){ param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) - # Add nested module bits - $nestedModules = [System.Collections.ArrayList]::new() - # Script module - New-Item -Path (Join-Path $moduleBase $scriptModuleFile) -Value $scriptModuleContent - # Binary module - Copy-Item -Path $binaryModuleSourcePath -Destination (Join-Path $moduleBase $binaryModuleFile) - # Root module - if ($UseRootModule) - { - New-Item -Path (Join-Path $moduleBase $rootModuleFile) -Value $rootModuleContent - } - - $nestedModules = $scriptModuleFile,$binaryModuleFile - $nestedModuleNames = $nestedModules -join ',' - - # Create the manifest - $moduleName = Split-Path -Leaf $moduleBase - $manifestPath = Join-Path $moduleBase "$moduleName.psd1" - $compatibleEditions = if ($CompatiblePSEditions) { $CompatiblePSEditions -join "," } - - $newManifestCmd = "New-ModuleManifest -Path $manifestPath -NestedModules $nestedModuleNames " - if ($compatibleEditions) { $newManifestCmd += "-CompatiblePSEditions $compatibleEditions " } - if ($UseRootModule) { $newManifestCmd += "-RootModule $rootModuleFile " } - [scriptblock]::Create($newManifestCmd).Invoke() + New-TestNestedModule ` + -ModuleBase $moduleBase ` + -ScriptModuleFilename $scriptModuleFile ` + -ScriptModuleContent $scriptModuleContent ` + -BinaryModuleFilename $binaryModuleFile ` + -BinaryModuleDllPath $binaryModuleSourcePath ` + -RootModuleFilename $rootModuleFile ` + -RootModuleContent $rootModuleContent ` + -CompatiblePSEditions $MarkedEdition ` + -UseRootModule $UseRootModule ` + -UseAbsolutePath $UseAbsolutePath # Modules specified with an absolute path should only return themselves - if ($UseAbsolutePath) - { - $modules = Get-Module -ListAvailable -All $moduleBase + if ($UseAbsolutePath) { + if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) + { + { Get-Module -ListAvailable -All $moduleBase -ErrorAction Stop } | Should -Throw -ErrorId "Modules_ModuleNotFoundForGetModule,Microsoft.PowerShell.Commands.GetModuleCommand" + return + } + + $modules = if ($SkipEditionCheck) + { + Get-Module -ListAvailable -All -SkipEditionCheck $moduleBase + } + else + { + Get-Module -ListAvailable -All $moduleBase + } $modules.Count | Should -Be 1 $modules[0].Name | Should -BeExactly $moduleName @@ -404,6 +453,12 @@ Describe "Nested module behaviour" -Tag "Feature" { Get-Module -ListAvailable -All | Where-Object { $_.Path.Contains($guid) } } + if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) + { + $modules.Count | Should -Be 0 + return + } + if ($UseRootModule) { $modules.Count | Should -Be 4 @@ -420,13 +475,14 @@ Describe "Nested module behaviour" -Tag "Feature" { } } - Context "Modules OFF the System32 test path" { + Context "Modules OFF the System32 module path" { BeforeEach { # Create the module directory $guid = New-Guid - $compatibilityDir = $incompatibleDir + $compatibilityDir = $compatibleDir $containingDir = Join-Path $TestDrive $compatibilityDir $guid - $moduleBase = Join-Path $containingDir "CpseTestModule" + $moduleName = "CpseTestModule" + $moduleBase = Join-Path $containingDir $moduleName New-Item -Path $moduleBase -ItemType Directory Add-ModulePath $containingDir } @@ -435,50 +491,25 @@ Describe "Nested module behaviour" -Tag "Feature" { Restore-ModulePath } - It "Gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows){ + It "Get-Module -ListAvailable -All gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows){ param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) - # Add nested module bits - $nestedModules = [System.Collections.ArrayList]::new() - # Script module - New-Item -Path (Join-Path $moduleBase $scriptModuleFile) -Value $scriptModuleContent - # Binary module - Copy-Item -Path $binaryModuleSourcePath -Destination (Join-Path $moduleBase $binaryModuleFile) - # Root module - if ($UseRootModule) - { - New-Item -Path (Join-Path $moduleBase $rootModuleFile) -Value $rootModuleContent - } - - $nestedModules = $scriptModuleFile,$binaryModuleFile - $nestedModuleNames = $nestedModules -join ',' - - # Create the manifest - $moduleName = Split-Path -Leaf $moduleBase - $manifestPath = Join-Path $moduleBase "$moduleName.psd1" - $compatibleEditions = if ($MarkedEdition) { $MarkedEdition -join "," } - - $newManifestCmd = "New-ModuleManifest -Path $manifestPath -NestedModules $nestedModuleNames " - if ($compatibleEditions) { $newManifestCmd += "-CompatiblePSEditions $compatibleEditions " } - if ($UseRootModule) { $newManifestCmd += "-RootModule $rootModuleFile " } - [scriptblock]::Create($newManifestCmd).Invoke() + New-TestNestedModule ` + -ModuleBase $moduleBase ` + -ScriptModuleFilename $scriptModuleFile ` + -ScriptModuleContent $scriptModuleContent ` + -BinaryModuleFilename $binaryModuleFile ` + -BinaryModuleDllPath $binaryModuleSourcePath ` + -RootModuleFilename $rootModuleFile ` + -RootModuleContent $rootModuleContent ` + -CompatiblePSEditions $MarkedEdition ` + -UseRootModule $UseRootModule ` + -UseAbsolutePath $UseAbsolutePath # Modules specified with an absolute path should only return themselves - if ($UseAbsolutePath) { - if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) - { - { Get-Module -ListAvailable -All $moduleBase -ErrorAction Stop } | Should -Throw -ErrorId "Modules_ModuleNotFoundForGetModule,Microsoft.PowerShell.Commands.GetModuleCommand" - return - } - - $modules = if ($SkipEditionCheck) - { - Get-Module -ListAvailable -All -SkipEditionCheck $moduleBase - } - else - { - Get-Module -ListAvailable -All $moduleBase - } + if ($UseAbsolutePath) + { + $modules = Get-Module -ListAvailable -All $moduleBase $modules.Count | Should -Be 1 $modules[0].Name | Should -BeExactly $moduleName @@ -494,12 +525,6 @@ Describe "Nested module behaviour" -Tag "Feature" { Get-Module -ListAvailable -All | Where-Object { $_.Path.Contains($guid) } } - if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) - { - $modules.Count | Should -Be 0 - return - } - if ($UseRootModule) { $modules.Count | Should -Be 4 From 073800e8a770baa86216bd8560a1b7403ea9b4f7 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 16:55:13 -0700 Subject: [PATCH 24/35] [Feature] Address @daxian-dbw's comments --- .../engine/Modules/AnalysisCache.cs | 2 +- .../engine/Modules/GetModuleCommand.cs | 4 ++-- .../engine/Modules/ModuleCmdletBase.cs | 2 +- src/System.Management.Automation/resources/Modules.resx | 3 +++ 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs index 8c67b7d04c5..654cd20be2d 100644 --- a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs +++ b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs @@ -482,7 +482,7 @@ internal static void CacheModuleExports(PSModuleInfo module, ExecutionContext co // Don't cache incompatible modules on the system32 module path even if loaded with // -SkipEditionCheck, since it will break subsequent sessions. - if (!module.IsConsideredEditionCompatible && ModuleUtils.IsOnSystem32ModulePath(module.ModuleBase)) + if (!module.IsConsideredEditionCompatible) { ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached."); return; diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index 964efc50d2f..1a14212c0a0 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -357,8 +357,8 @@ protected override void ProcessRecord() if (SkipEditionCheck && !ListAvailable) { ErrorRecord error = new ErrorRecord( - new InvalidOperationException("-SkipEditionCheck can only be used with -ListAvailable"), - "SkipEditionCheckCannotBeSpecifiedWithoutListAvailable", + new InvalidOperationException(Modules.SkipEditionCheckNotSupportedWithoutListAvailable), + nameof(Modules.SkipEditionCheckNotSupportedWithoutListAvailable), ErrorCategory.InvalidOperation, targetObject: null); diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 1c0d04da14f..6891df83f4f 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2422,7 +2422,7 @@ internal PSModuleInfo LoadModuleManifest( { // If we're trying to load the module, return null so that caches // are not polluted - if (manifestProcessingFlags.HasFlag(ManifestProcessingFlags.LoadElements)) + if (importingModule) { return null; } diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index bad7511e790..f88928b3592 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -615,4 +615,7 @@ One or more invalid experimental feature names found: {0}. A module experimental feature name should follow this convention: 'ModuleName.FeatureName'. + + The -SkipEditionCheck switch parameter cannot be used without the -ListAvailable switch parameter. + From f8ccb9f84755023afa52815d8dacc924757e61b1 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 17:24:02 -0700 Subject: [PATCH 25/35] [Feature] Refactor PSEdition support check into static ModuleUtils method --- .../engine/Modules/ModuleCmdletBase.cs | 40 +------- .../engine/Modules/ModuleUtils.cs | 91 +++++++++++++------ 2 files changed, 64 insertions(+), 67 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 6891df83f4f..cd6ce7f1369 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2394,8 +2394,8 @@ internal PSModuleInfo LoadModuleManifest( // On Windows, we want to include any modules under %WINDIR%\System32\WindowsPowerShell\v1.0\Modules // that have declared compatibility with PS Core (or if the check is skipped) IEnumerable inferredCompatiblePSEditions = compatiblePSEditions ?? DefaultCompatiblePSEditions; - if (!IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions, - out bool declaresCoreCompatible, out bool isOnSystem32ModulePath)) + bool isConsideredCompatible = BaseSkipEditionCheck || ModuleUtils.IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions); + if (!isConsideredCompatible) { containedErrors = true; @@ -2538,7 +2538,7 @@ internal PSModuleInfo LoadModuleManifest( // A module is considered compatible if it's not on the System32 module path, or // if it is and declared "Core" as a compatible PSEdition. - manifestInfo.IsConsideredEditionCompatible = declaresCoreCompatible || !isOnSystem32ModulePath; + manifestInfo.IsConsideredEditionCompatible = isConsideredCompatible; if (expFeatureList != null) { @@ -3446,40 +3446,6 @@ internal PSModuleInfo LoadModuleManifest( return manifestInfo; } - /// - /// Check if the CompatiblePSEditions field of a given module - /// declares compatibility with the running PowerShell edition. - /// - /// The path to the module manifest being checked. - /// The value of the CompatiblePSEditions field of the module manifest. - /// - /// True if the module explicitly declares compatiblity for the current PSEdition in its manifest, otherwise false. - /// - /// True if the module is on the system32 module path (where edition compatibility is checked), false otherwise. - /// - /// True if the module is compatible with the running PowerShell edition, false otherwise. - private bool IsPSEditionCompatible( - string moduleManifestPath, - IEnumerable compatiblePSEditions, - out bool moduleDeclaresCoreCompatible, - out bool isOnSystem32ModulePath) - { - moduleDeclaresCoreCompatible = Utils.IsPSEditionSupported(compatiblePSEditions); - isOnSystem32ModulePath = false; -#if UNIX - return true; -#else - if (!ModuleUtils.IsOnSystem32ModulePath(moduleManifestPath)) - { - return true; - } - - isOnSystem32ModulePath = true; - - return BaseSkipEditionCheck || moduleDeclaresCoreCompatible; -#endif - } - private static void PropagateExportedTypesFromNestedModulesToRootModuleScope(ImportModuleOptions options, PSModuleInfo manifestInfo) { if (manifestInfo.NestedModules == null) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 1decef12a5b..f343a3f0ade 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -73,7 +73,7 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto { if (!Directory.Exists(topDirectoryToCheck)) { yield break; } - if (IsIncompatibleSystem32ModuleDir(topDirectoryToCheck, skipEditionCheck)) { yield break; } + if (!skipEditionCheck && IsIncompatibleSystem32ModuleDir(topDirectoryToCheck)) { yield break; } var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions; Queue directoriesToCheck = new Queue(); @@ -87,7 +87,7 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto string[] subDirectories = Directory.GetDirectories(directoryToCheck, "*", options); foreach (string toAdd in subDirectories) { - if (IsPossibleModuleDirectory(toAdd) && !IsIncompatibleSystem32ModuleDir(toAdd, skipEditionCheck)) + if (IsPossibleModuleDirectory(toAdd) && (skipEditionCheck || !IsIncompatibleSystem32ModuleDir(toAdd))) { directoriesToCheck.Enqueue(toAdd); } @@ -116,50 +116,56 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto /// System32 module path. /// /// The path of the directory to check. - /// If true, skips the check and returns false. /// /// If the directory is the root of a module on the System32 path and not marked as /// Core-compatible, returns true, otherwise returns false. /// - internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath, bool skipEditionCheck) + internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath) { - // Skip the check means assume compatible - if (skipEditionCheck) - { - return false; - } - // Not on System32 path means assume compatible if (!ModuleUtils.IsOnSystem32ModulePath(directoryPath)) { return false; } + // Work out the psd1 path + string dirName = Path.GetFileName(directoryPath.TrimEnd('/', '\\')); + string manifestPath = Path.Join(directoryPath, dirName + ".psd1"); + + // Check the manifest file for compatibility + return !Utils.IsPSEditionSupported(ReadCompatiblePSEditionsFromManifest(manifestPath)); + } + + /// + /// Try to read in the CompatiblePSEditions from a module file, lazily. + /// + /// The path to the module manifest to proces. + /// All PSEditions listed as compatible by the manifest, if any. + private static IEnumerable ReadCompatiblePSEditionsFromManifest(string manifestPath) + { Hashtable manifest; - try + + if (!File.Exists(manifestPath)) { - string dirName = Path.GetFileName(directoryPath.TrimEnd('/', '\\')); - string manifestPath = Path.Join(directoryPath, dirName + ".psd1"); - if (!File.Exists(manifestPath)) - { - // If there's no manifest, it might be an ordinary directory, so not incompatible - return false; - } + // No manifest => no supported editions + yield break; + } + try + { manifest = PsUtils.GetModuleManifestProperties(manifestPath, new [] { "CompatiblePSEditions" }); } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) { - // Take the safe option if we hit an exception - return false; + // If the file cannot be accessed, treat it as if it doesn't exist + yield break; } object psEditionsObj = manifest?["CompatiblePSEditions"]; if (psEditionsObj == null) { - // If there is a manifest file but not "CompatiblePSEditions" key, - // the module is incompatible - return true; + // No compatibility field => no supported editions + yield break; } string[] psEditions; @@ -169,22 +175,47 @@ internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath, bool } catch (PSInvalidCastException) { - // If the "CompatiblePSEditions" key exists but isn't a string[], - // the module is bad - return true; + // If the field exists but can't be cast to a string array, ignore it + yield break; } if (psEditions == null) { - // If somehow the key was set but set as $null, that's the same as not setting it - // Meaning incompatible + // The field exists but is null => ignore it + yield break; + } + + // Finally return the supported editions entry + foreach (string psEdition in psEditions) + { + yield return psEdition; + } + } + + /// + /// Check if the CompatiblePSEditions field of a given module + /// declares compatibility with the running PowerShell edition. + /// + /// The path to the module manifest being checked. + /// The value of the CompatiblePSEditions field of the module manifest. + /// True if the module is compatible with the running PowerShell edition, false otherwise. + internal static bool IsPSEditionCompatible( + string moduleManifestPath, + IEnumerable compatiblePSEditions) + { +#if UNIX + return true; +#else + if (!ModuleUtils.IsOnSystem32ModulePath(moduleManifestPath)) + { return true; } - // Finally check the edition flags of the module - return !Utils.IsPSEditionSupported(psEditions); + return Utils.IsPSEditionSupported(compatiblePSEditions); +#endif } + internal static IEnumerable GetDefaultAvailableModuleFiles(bool isForAutoDiscovery, ExecutionContext context) { HashSet uniqueModuleFiles = new HashSet(StringComparer.OrdinalIgnoreCase); From 26af658cbeafc3df1d3d74563dab40cbf5304ccf Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 18:24:50 -0700 Subject: [PATCH 26/35] [Feature] Fix refactor bug, add Get-Module tests --- .../engine/Modules/ModuleUtils.cs | 15 ++- .../CompatiblePSEditions.Module.Tests.ps1 | 119 +++++++++++++++++- 2 files changed, 121 insertions(+), 13 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index f343a3f0ade..60dbfb2f355 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -132,6 +132,12 @@ internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath) string dirName = Path.GetFileName(directoryPath.TrimEnd('/', '\\')); string manifestPath = Path.Join(directoryPath, dirName + ".psd1"); + // No manifest means this is not a module directory + if (!File.Exists(manifestPath)) + { + return false; + } + // Check the manifest file for compatibility return !Utils.IsPSEditionSupported(ReadCompatiblePSEditionsFromManifest(manifestPath)); } @@ -144,18 +150,11 @@ internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath) private static IEnumerable ReadCompatiblePSEditionsFromManifest(string manifestPath) { Hashtable manifest; - - if (!File.Exists(manifestPath)) - { - // No manifest => no supported editions - yield break; - } - try { manifest = PsUtils.GetModuleManifestProperties(manifestPath, new [] { "CompatiblePSEditions" }); } - catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is FileNotFoundException) { // If the file cannot be accessed, treat it as if it doesn't exist yield break; diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 43a159a57cd..9f89851a805 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -265,7 +265,9 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { It "Fails to import incompatible modules from the module path with PSEdition " -TestCases $failCases -Skip:(-not $IsWindows) { param($Editions, $ModuleName, $Result) - { Import-Module $ModuleName -Force -ErrorAction 'Stop'; & "Test-$ModuleName" } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" + { + Import-Module $ModuleName -Force -ErrorAction 'Stop'; & "Test-$ModuleName" + } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" } It "Imports an incompatible module from the module path with -SkipEditionCheck with PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { @@ -291,7 +293,9 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { $path = Join-Path -Path $basePath -ChildPath $ModuleName - { Import-Module $path -Force -ErrorAction 'Stop'; & "Test-$ModuleName" } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" + { + Import-Module $path -Force -ErrorAction 'Stop'; & "Test-$ModuleName" + } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" } It "Imports an incompatible module from an absolute path with -SkipEditionCheck with PSEdition " -TestCases ($successCases + $failCases) -Skip:(-not $IsWindows) { @@ -383,7 +387,7 @@ Describe "Nested module behaviour" -Tag "Feature" { } } - Context "Modules OFF the System32 test path" { + Context "Modules ON the System32 test path" { BeforeAll { [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $incompatiblePath) } @@ -407,6 +411,64 @@ Describe "Nested module behaviour" -Tag "Feature" { Restore-ModulePath } + It "Get-Module -ListAvailable gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows) { + param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) + + New-TestNestedModule ` + -ModuleBase $moduleBase ` + -ScriptModuleFilename $scriptModuleFile ` + -ScriptModuleContent $scriptModuleContent ` + -BinaryModuleFilename $binaryModuleFile ` + -BinaryModuleDllPath $binaryModuleSourcePath ` + -RootModuleFilename $rootModuleFile ` + -RootModuleContent $rootModuleContent ` + -CompatiblePSEditions $MarkedEdition ` + -UseRootModule $UseRootModule ` + -UseAbsolutePath $UseAbsolutePath + + if ($UseAbsolutePath) + { + if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) + { + Get-Module -ListAvailable $moduleBase -ErrorAction Stop | Should -Be $null + return + } + + $modules = if ($SkipEditionCheck) + { + Get-Module -ListAvailable $moduleBase -SkipEditionCheck + } + else + { + Get-Module -ListAvailable $moduleBase + } + + $modules.Count | Should -Be 1 + $modules[0].Name | Should -Be $moduleName + return + } + + $modules = if ($SkipEditionCheck) + { + Get-Module -ListAvailable -SkipEditionCheck + } + else + { + Get-Module -ListAvailable + } + + $modules = $modules | Where-Object { $_.Path.Contains($guid) } + + if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) + { + $modules.Count | Should -Be 0 + return + } + + $modules.Count | Should -Be 1 + $modules[0].Name | Should -Be $moduleName + } + It "Get-Module -ListAvailable -All gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows){ param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) @@ -426,7 +488,9 @@ Describe "Nested module behaviour" -Tag "Feature" { if ($UseAbsolutePath) { if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) { - { Get-Module -ListAvailable -All $moduleBase -ErrorAction Stop } | Should -Throw -ErrorId "Modules_ModuleNotFoundForGetModule,Microsoft.PowerShell.Commands.GetModuleCommand" + { + Get-Module -ListAvailable -All $moduleBase -ErrorAction Stop + } | Should -Throw -ErrorId "Modules_ModuleNotFoundForGetModule,Microsoft.PowerShell.Commands.GetModuleCommand" return } @@ -491,7 +555,52 @@ Describe "Nested module behaviour" -Tag "Feature" { Restore-ModulePath } - It "Get-Module -ListAvailable -All gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows){ + It "Get-Module -ListAvailable gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases { + param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) + + New-TestNestedModule ` + -ModuleBase $moduleBase ` + -ScriptModuleFilename $scriptModuleFile ` + -ScriptModuleContent $scriptModuleContent ` + -BinaryModuleFilename $binaryModuleFile ` + -BinaryModuleDllPath $binaryModuleSourcePath ` + -RootModuleFilename $rootModuleFile ` + -RootModuleContent $rootModuleContent ` + -CompatiblePSEditions $MarkedEdition ` + -UseRootModule $UseRootModule ` + -UseAbsolutePath $UseAbsolutePath + + if ($UseAbsolutePath) + { + $modules = if ($SkipEditionCheck) + { + Get-Module -ListAvailable $moduleBase -SkipEditionCheck + } + else + { + Get-Module -ListAvailable $moduleBase + } + + $modules.Count | Should -Be 1 + $modules[0].Name | Should -Be $moduleName + return + } + + $modules = if ($SkipEditionCheck) + { + Get-Module -ListAvailable -SkipEditionCheck + } + else + { + Get-Module -ListAvailable + } + + $modules = $modules | Where-Object { $_.Path.Contains($guid) } + $modules.Count | Should -Be 1 + $modules[0].Name | Should -Be $moduleName + } + + It "Get-Module -ListAvailable -All gets all compatible modules when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases { param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) New-TestNestedModule ` From 3c59b555ecf66914efa6a28fd09ac97373a4d6cf Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 18:57:09 -0700 Subject: [PATCH 27/35] [Feature] Add comment to CompatiblePSEditions to document field --- .../engine/Modules/PSModuleInfo.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index d110239678a..58fa594e8e8 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -820,7 +820,10 @@ internal void AddToFileList(string file) } /// - /// CompatiblePSEditions + /// Lists the PowerShell editions this module is compatible with. This should + /// reflect the module manifest the module was loaded with, or if no manifest was given + /// or the key was not in the manifest, this should be an empty collection. This + /// property is never null. /// public IEnumerable CompatiblePSEditions { From a0e1ca43ec9247bb1b5bbd58763903e1a997cb7d Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 13 Jul 2018 19:44:15 -0700 Subject: [PATCH 28/35] [Feature] Add Import-Module tests for nested modules --- .../CompatiblePSEditions.Module.Tests.ps1 | 234 +++++++++++++++++- 1 file changed, 224 insertions(+), 10 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 9f89851a805..60e78bb0f93 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -83,11 +83,18 @@ function New-TestNestedModule [bool]$UseAbsolutePath ) + $nestedModules = [System.Collections.ArrayList]::new() + # Create script module New-Item -Path (Join-Path $ModuleBase $ScriptModuleFileName) -Value $ScriptModuleContent + $nestedModules.Add($ScriptModuleFilename) - # Create binary module - Copy-Item -Path $BinaryModuleDllPath -Destination (Join-Path $ModuleBase $BinaryModuleFilename) + if ($BinaryModuleFilename -and $BinaryModuleDllPath) + { + # Create binary module + Copy-Item -Path $BinaryModuleDllPath -Destination (Join-Path $ModuleBase $BinaryModuleFilename) + $nestedModules.Add($BinaryModuleFilename) + } # Create the root module if there is one if ($UseRootModule) @@ -99,7 +106,7 @@ function New-TestNestedModule $moduleName = Split-Path -Leaf $ModuleBase $manifestPath = Join-Path $ModuleBase "$moduleName.psd1" - $nestedModules = $ScriptModuleFilename,$BinaryModuleFilename -join ',' + $nestedModules = $nestedModules -join ',' $newManifestCmd = "New-ModuleManifest -Path $manifestPath -NestedModules $nestedModules " if ($CompatiblePSEditions) @@ -110,6 +117,11 @@ function New-TestNestedModule if ($UseRootModule) { $newManifestCmd += "-RootModule $RootModuleFilename " + $newManifestCmd += "-FunctionsToExport @('Test-RootModule') " + } + else + { + $newManifestCmd += "-FunctionsToExport @('Test-ScriptModule') " } # Create the manifest @@ -329,13 +341,8 @@ Describe "PSModulePath changes interacting with other PowerShell processes" -Tag } } -Describe "Nested module behaviour" -Tag "Feature" { +Describe "Get-Module nested module behaviour with Edition checking" -Tag "Feature" { BeforeAll { - if (-not $IsWindows) - { - return - } - $testConditions = @{ SkipEditionCheck = @($true, $false) UseRootModule = @($true, $false) @@ -373,7 +380,7 @@ Describe "Nested module behaviour" -Tag "Feature" { # Define root module definition $rootModuleName = "RootModule" $rootModuleFile = "$rootModuleName.psm1" - $rootModuleContent = 'function Test-RootModule { Test-ScriptModule; [TestBinaryModuleClass]::Test() }' + $rootModuleContent = 'function Test-RootModule { Test-ScriptModule }' # Module directory structure: $TestDrive/$compatibility/$guid/$moduleName/{module parts} $compatibleDir = "Compatible" @@ -650,3 +657,210 @@ Describe "Nested module behaviour" -Tag "Feature" { } } } + +Describe "Import-Module nested module behaviour with Edition checking" -Tag "Feature" { + BeforeAll { + $testConditions = @{ + SkipEditionCheck = @($true, $false) + UseRootModule = @($true, $false) + UseAbsolutePath = @($true, $false) + MarkedEdition = @($null, "Desktop", "Core", @("Desktop","Core")) + } + + # Combine all the test conditions into a list of test cases + $testCases = @(@{}) + foreach ($condition in $testConditions.Keys) + { + $list = [System.Collections.Generic.List[hashtable]]::new() + foreach ($obj in $testCases) + { + foreach ($value in $testConditions[$condition]) + { + $list.Add($obj + @{ $condition = $value }) + } + } + $testCases = $list + } + + # Define nested script module + $scriptModuleName = "NestedScriptModule" + $scriptModuleFile = "$scriptModuleName.psm1" + $scriptModuleContent = 'function Test-ScriptModule { return $true }' + + # Define root module definition + $rootModuleName = "RootModule" + $rootModuleFile = "$rootModuleName.psm1" + $rootModuleContent = 'function Test-RootModule { Test-ScriptModule }' + + # Module directory structure: $TestDrive/$compatibility/$guid/$moduleName/{module parts} + $compatibleDir = "Compatible" + $incompatibleDir = "Incompatible" + $compatiblePath = Join-Path $TestDrive $compatibleDir + $incompatiblePath = Join-Path $TestDrive $incompatibleDir + + foreach ($basePath in $compatiblePath,$incompatiblePath) + { + New-Item -Path $basePath -ItemType Directory + } + } + + Context "Modules ON the System32 test path" { + BeforeAll { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $incompatiblePath) + } + + AfterAll { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook("TestWindowsPowerShellPSHomeLocation", $null) + } + + BeforeEach { + # Create the module directory + $guid = New-Guid + $compatibilityDir = $incompatibleDir + $containingDir = Join-Path $TestDrive $compatibilityDir $guid + $moduleName = "CpseTestModule" + $moduleBase = Join-Path $containingDir $moduleName + New-Item -Path $moduleBase -ItemType Directory + Add-ModulePath $containingDir + } + + AfterEach { + Get-Module $moduleName | Remove-Module -Force + Restore-ModulePath + } + + It "Import-Module when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases -Skip:(-not $IsWindows) { + param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) + + New-TestNestedModule ` + -ModuleBase $moduleBase ` + -ScriptModuleFilename $scriptModuleFile ` + -ScriptModuleContent $scriptModuleContent ` + -RootModuleFilename $rootModuleFile ` + -RootModuleContent $rootModuleContent ` + -CompatiblePSEditions $MarkedEdition ` + -UseRootModule $UseRootModule ` + -UseAbsolutePath $UseAbsolutePath + + if ($UseAbsolutePath) + { + if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) + { + { + Import-Module $moduleBase -ErrorAction Stop + } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" + return + } + + if ($SkipEditionCheck) + { + Import-Module $moduleBase -SkipEditionCheck + } + else + { + Import-Module $moduleBase + } + + if ($UseRootModule) + { + Test-RootModule | Should -BeTrue + { Test-ScriptModule } | Should -Throw -ErrorId "CommandNotFoundException" + return + } + + Test-ScriptModule | Should -BeTrue + { Test-RootModule } | Should -Throw -ErrorId "CommandNotFoundException" + return + } + + if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) + { + { + Import-Module $moduleName -ErrorAction Stop + } | Should -Throw -ErrorId "Modules_PSEditionNotSupported,Microsoft.PowerShell.Commands.ImportModuleCommand" + return + } + + if ($SkipEditionCheck) + { + Import-Module $moduleName -SkipEditionCheck + } + else + { + Import-Module $moduleName + } + + if ($UseRootModule) + { + Test-RootModule | Should -BeTrue + { Test-ScriptModule } | Should -Throw -ErrorId "CommandNotFoundException" + return + } + + Test-ScriptModule | Should -BeTrue + { Test-RootModule } | Should -Throw -ErrorId "CommandNotFoundException" + } + } + + Context "Modules OFF the System32 module path" { + BeforeEach { + # Create the module directory + $guid = New-Guid + $compatibilityDir = $compatibleDir + $containingDir = Join-Path $TestDrive $compatibilityDir $guid + $moduleName = "CpseTestModule" + $moduleBase = Join-Path $containingDir $moduleName + New-Item -Path $moduleBase -ItemType Directory + Add-ModulePath $containingDir + } + + AfterEach { + Get-Module $moduleName | Remove-Module -Force + Restore-ModulePath + } + + It "Import-Module when SkipEditionCheck: , using root module: , using absolute path: , CompatiblePSEditions: " -TestCases $testCases { + param([bool]$SkipEditionCheck, [bool]$UseRootModule, [bool]$UseAbsolutePath, [string[]]$MarkedEdition) + + New-TestNestedModule ` + -ModuleBase $moduleBase ` + -ScriptModuleFilename $scriptModuleFile ` + -ScriptModuleContent $scriptModuleContent ` + -RootModuleFilename $rootModuleFile ` + -RootModuleContent $rootModuleContent ` + -CompatiblePSEditions $MarkedEdition ` + -UseRootModule $UseRootModule ` + -UseAbsolutePath $UseAbsolutePath + + if ($UseAbsolutePath) + { + if ($SkipEditionCheck) + { + Import-Module $moduleBase -SkipEditionCheck + } + else + { + Import-Module $moduleBase + } + } + elseif ($SkipEditionCheck) + { + Import-Module $moduleName -SkipEditionCheck + } + else + { + Import-Module $moduleName + } + + if ($UseRootModule) + { + Test-RootModule | Should -BeTrue + { Test-ScriptModule } | Should -Throw -ErrorId "CommandNotFoundException" + return + } + + Test-ScriptModule | Should -BeTrue + { Test-RootModule } | Should -Throw -ErrorId "CommandNotFoundException" + } + } +} From 81cb1109dcc508cbe2934c4ed5c9f05d3c31db6b Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Sun, 15 Jul 2018 13:27:05 -0700 Subject: [PATCH 29/35] [Feature] Add comment in PSModuleInfo about loading psm1s from System32 path --- .../engine/Modules/PSModuleInfo.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index 58fa594e8e8..2a2534a0f09 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -846,7 +846,10 @@ internal void AddToCompatiblePSEditions(IEnumerable psEditions) /// Describes whether the module was considered compatible at load time. /// Any module not on the System32 module path should have this as true. /// Modules loaded from the System32 module path will have this as true if they - /// have declared edition compatibility with PowerShell Core. + /// have declared edition compatibility with PowerShell Core. Currently, this field + /// is true for all non-psd1 module files, when it should not be. Being able to + /// load psm1/dll modules from the System32 module path without needing to skip + /// the edition check is considered a bug and should be fixed. /// internal bool IsConsideredEditionCompatible { get; set; } = true; From 87a6c3266fd527ab25bebb283c85be2974a12d5e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Sun, 15 Jul 2018 13:28:58 -0700 Subject: [PATCH 30/35] [Feature] Skip Before/AfterAll blocks on UNIX --- .../CompatiblePSEditions.Module.Tests.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 60e78bb0f93..2ff72821d77 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -323,10 +323,18 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" { Describe "PSModulePath changes interacting with other PowerShell processes" -Tag "Feature" { BeforeAll { + if (-not $IsWindows) + { + return + } Add-ModulePath (Join-Path $env:windir "System32\WindowsPowerShell\v1.0\Modules") -Prepend } AfterAll { + if (-not $IsWindows) + { + return + } Restore-ModulePath } From 6d72598bde08ca2507cd357b5723204ec4941541 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 16 Jul 2018 13:30:30 -0700 Subject: [PATCH 31/35] [Feature] Make PSModuleInfo.IsConsideredEditionCompatible not depend on SkipEditionCheck --- .../engine/Modules/ModuleCmdletBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index cd6ce7f1369..0c8b111ada7 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2394,8 +2394,8 @@ internal PSModuleInfo LoadModuleManifest( // On Windows, we want to include any modules under %WINDIR%\System32\WindowsPowerShell\v1.0\Modules // that have declared compatibility with PS Core (or if the check is skipped) IEnumerable inferredCompatiblePSEditions = compatiblePSEditions ?? DefaultCompatiblePSEditions; - bool isConsideredCompatible = BaseSkipEditionCheck || ModuleUtils.IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions); - if (!isConsideredCompatible) + bool isConsideredCompatible = ModuleUtils.IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions); + if (!BaseSkipEditionCheck && !isConsideredCompatible) { containedErrors = true; From d0ac17ac7f1338101ad31bde63079dba70b57b98 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 16 Jul 2018 14:58:53 -0700 Subject: [PATCH 32/35] [Feature] Fix Get-Module bug where loaded incompatible modules aren't listed --- .../engine/Modules/GetModuleCommand.cs | 2 +- .../CompatiblePSEditions.Module.Tests.ps1 | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index 1a14212c0a0..5bb676ea759 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -505,7 +505,7 @@ private IEnumerable FilterModulesForEditionAndSpecification( { #if !UNIX // Edition check only applies to Windows System32 module path - if (!SkipEditionCheck) + if (!SkipEditionCheck && ListAvailable) { modules = modules.Where(module => module.IsConsideredEditionCompatible); } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index 2ff72821d77..ea744a2c2af 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -124,6 +124,8 @@ function New-TestNestedModule $newManifestCmd += "-FunctionsToExport @('Test-ScriptModule') " } + $newManifestCmd += "-CmdletsToExport @() -VariablesToExport @() -AliasesToExport @() " + # Create the manifest [scriptblock]::Create($newManifestCmd).Invoke() } From e434a143f83ed321ab3cc52b94b869f45ae18a45 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 16 Jul 2018 15:59:03 -0700 Subject: [PATCH 33/35] [Feature] Revert changes to Get-Module -ListAvailable -All --- .../engine/Modules/GetModuleCommand.cs | 2 +- .../engine/Modules/ModuleCmdletBase.cs | 4 +- .../engine/Modules/ModuleUtils.cs | 37 +------------------ .../CompatiblePSEditions.Module.Tests.ps1 | 14 ------- 4 files changed, 5 insertions(+), 52 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index 5bb676ea759..c6745a4a44c 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -505,7 +505,7 @@ private IEnumerable FilterModulesForEditionAndSpecification( { #if !UNIX // Edition check only applies to Windows System32 module path - if (!SkipEditionCheck && ListAvailable) + if (!SkipEditionCheck && ListAvailable && !All) { modules = modules.Where(module => module.IsConsideredEditionCompatible); } diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 0c8b111ada7..219f9c9f19d 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -946,7 +946,7 @@ private IEnumerable GetModuleForRootedPaths(List modulePat } var availableModuleFiles = all - ? ModuleUtils.GetAllAvailableModuleFiles(resolvedModulePath, BaseSkipEditionCheck) + ? ModuleUtils.GetAllAvailableModuleFiles(resolvedModulePath) : ModuleUtils.GetModuleFilesFromAbsolutePath(resolvedModulePath); bool foundModule = false; @@ -1048,7 +1048,7 @@ private IEnumerable GetModulesFromOneModulePath(List names } IEnumerable moduleFiles = all - ? ModuleUtils.GetAllAvailableModuleFiles(modulePath, BaseSkipEditionCheck) + ? ModuleUtils.GetAllAvailableModuleFiles(modulePath) : ModuleUtils.GetDefaultAvailableModuleFiles(modulePath); foreach (string file in moduleFiles) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 60dbfb2f355..2d317102673 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -69,12 +69,10 @@ internal static bool IsPossibleModuleDirectory(string dir) /// Get all module files by searching the given directory recursively. /// All sub-directories that could be a module folder will be searched. /// - internal static IEnumerable GetAllAvailableModuleFiles(string topDirectoryToCheck, bool skipEditionCheck) + internal static IEnumerable GetAllAvailableModuleFiles(string topDirectoryToCheck) { if (!Directory.Exists(topDirectoryToCheck)) { yield break; } - if (!skipEditionCheck && IsIncompatibleSystem32ModuleDir(topDirectoryToCheck)) { yield break; } - var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions; Queue directoriesToCheck = new Queue(); directoriesToCheck.Enqueue(topDirectoryToCheck); @@ -87,7 +85,7 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto string[] subDirectories = Directory.GetDirectories(directoryToCheck, "*", options); foreach (string toAdd in subDirectories) { - if (IsPossibleModuleDirectory(toAdd) && (skipEditionCheck || !IsIncompatibleSystem32ModuleDir(toAdd))) + if (IsPossibleModuleDirectory(toAdd)) { directoriesToCheck.Enqueue(toAdd); } @@ -111,37 +109,6 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto } } - /// - /// Checks a directory to see if it's the root of a Core-incompatible module on the - /// System32 module path. - /// - /// The path of the directory to check. - /// - /// If the directory is the root of a module on the System32 path and not marked as - /// Core-compatible, returns true, otherwise returns false. - /// - internal static bool IsIncompatibleSystem32ModuleDir(string directoryPath) - { - // Not on System32 path means assume compatible - if (!ModuleUtils.IsOnSystem32ModulePath(directoryPath)) - { - return false; - } - - // Work out the psd1 path - string dirName = Path.GetFileName(directoryPath.TrimEnd('/', '\\')); - string manifestPath = Path.Join(directoryPath, dirName + ".psd1"); - - // No manifest means this is not a module directory - if (!File.Exists(manifestPath)) - { - return false; - } - - // Check the manifest file for compatibility - return !Utils.IsPSEditionSupported(ReadCompatiblePSEditionsFromManifest(manifestPath)); - } - /// /// Try to read in the CompatiblePSEditions from a module file, lazily. /// diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 index ea744a2c2af..3861c1b7c0a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -503,14 +503,6 @@ Describe "Get-Module nested module behaviour with Edition checking" -Tag "Featur # Modules specified with an absolute path should only return themselves if ($UseAbsolutePath) { - if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) - { - { - Get-Module -ListAvailable -All $moduleBase -ErrorAction Stop - } | Should -Throw -ErrorId "Modules_ModuleNotFoundForGetModule,Microsoft.PowerShell.Commands.GetModuleCommand" - return - } - $modules = if ($SkipEditionCheck) { Get-Module -ListAvailable -All -SkipEditionCheck $moduleBase @@ -534,12 +526,6 @@ Describe "Get-Module nested module behaviour with Edition checking" -Tag "Featur Get-Module -ListAvailable -All | Where-Object { $_.Path.Contains($guid) } } - if ((-not $SkipEditionCheck) -and (-not ($MarkedEdition -contains "Core"))) - { - $modules.Count | Should -Be 0 - return - } - if ($UseRootModule) { $modules.Count | Should -Be 4 From 020123ea80b3d1f1fb9dda87c3d7a880dfdf26da Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 16 Jul 2018 16:02:26 -0700 Subject: [PATCH 34/35] [Feature] Remove unused method --- .../engine/Modules/ModuleUtils.cs | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 2d317102673..24d864b5f16 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -109,55 +109,6 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto } } - /// - /// Try to read in the CompatiblePSEditions from a module file, lazily. - /// - /// The path to the module manifest to proces. - /// All PSEditions listed as compatible by the manifest, if any. - private static IEnumerable ReadCompatiblePSEditionsFromManifest(string manifestPath) - { - Hashtable manifest; - try - { - manifest = PsUtils.GetModuleManifestProperties(manifestPath, new [] { "CompatiblePSEditions" }); - } - catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is FileNotFoundException) - { - // If the file cannot be accessed, treat it as if it doesn't exist - yield break; - } - - object psEditionsObj = manifest?["CompatiblePSEditions"]; - if (psEditionsObj == null) - { - // No compatibility field => no supported editions - yield break; - } - - string[] psEditions; - try - { - psEditions = LanguagePrimitives.ConvertTo(psEditionsObj); - } - catch (PSInvalidCastException) - { - // If the field exists but can't be cast to a string array, ignore it - yield break; - } - - if (psEditions == null) - { - // The field exists but is null => ignore it - yield break; - } - - // Finally return the supported editions entry - foreach (string psEdition in psEditions) - { - yield return psEdition; - } - } - /// /// Check if the CompatiblePSEditions field of a given module /// declares compatibility with the running PowerShell edition. @@ -181,7 +132,6 @@ internal static bool IsPSEditionCompatible( #endif } - internal static IEnumerable GetDefaultAvailableModuleFiles(bool isForAutoDiscovery, ExecutionContext context) { HashSet uniqueModuleFiles = new HashSet(StringComparer.OrdinalIgnoreCase); From a034a4aa1317693ea3b1c73e14c68d06481e2766 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 16 Jul 2018 16:19:31 -0700 Subject: [PATCH 35/35] [Feature] Remove unused using directives from ModuleUtils.cs --- .../engine/Modules/ModuleUtils.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 24d864b5f16..51153100ec0 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -1,12 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Management.Automation.Runspaces; using Dbg = System.Management.Automation.Diagnostics; @@ -405,7 +402,6 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe ) ) { - foreach (string modulePath in GetDefaultAvailableModuleFiles(isForAutoDiscovery: false, context)) { // Skip modules that have already been loaded so that we don't expose private commands.