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 7d7906340c1..b1ad7815a83 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs @@ -1196,11 +1196,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/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 5bac5a9ae86..47e2d74f9b9 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(); @@ -413,6 +413,12 @@ internal static List CompleteModuleName(CompletionContext cont if (!loadedModulesOnly) { powershell.AddParameter("ListAvailable", true); + + // -SkipEditionCheck should only be set or apply to -ListAvailable + if (skipEditionCheck) + { + powershell.AddParameter("SkipEditionCheck", true); + } } Exception exceptionThrown; @@ -2101,17 +2107,19 @@ private static void NativeCommandArgumentCompletion( case "Get-Module": { bool loadedModulesOnly = boundArguments == null || !boundArguments.ContainsKey("ListAvailable"); - NativeCompletionModuleCommands(context, parameterName, loadedModulesOnly, /* isImportModule: */ false, result); + bool skipEditionCheck = !loadedModulesOnly && boundArguments.ContainsKey("SkipEditionCheck"); + NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly, skipEditionCheck: skipEditionCheck); break; } case "Remove-Module": { - NativeCompletionModuleCommands(context, parameterName, /* loadedModulesOnly: */ true, /* isImportModule: */ false, result); + NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly: true); 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 +3054,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 +3081,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 +3093,7 @@ private static void NativeCompletionModuleCommands(CompletionContext context, st } } - var moduleResults = CompleteModuleName(context, loadedModulesOnly); + var moduleResults = CompleteModuleName(context, loadedModulesOnly, skipEditionCheck); if (moduleResults != null && moduleResults.Count > 0) result.AddRange(moduleResults); diff --git a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs index b39d52488ed..654cd20be2d 100644 --- a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs +++ b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs @@ -102,6 +102,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 the Windows System32 legacy module path and is incompatible with current PowerShell edition, skipping module: {modulePath}"); + return null; + } + Version version; if (ModuleUtils.IsModuleInVersionSubdirectory(modulePath, out version)) { @@ -160,6 +166,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.IsOnSystem32ModulePath(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; @@ -449,6 +480,14 @@ internal static void CacheModuleExports(PSModuleInfo module, ExecutionContext co { ModuleIntrinsics.Tracer.WriteLine("Requested caching for {0}", module.Name); + // Don't cache incompatible modules on the system32 module path even if loaded with + // -SkipEditionCheck, since it will break subsequent sessions. + if (!module.IsConsideredEditionCompatible) + { + ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached."); + return; + } + DateTime lastWriteTime; ModuleCacheEntry moduleCacheEntry; GetModuleEntryFromCache(module.Path, out lastWriteTime, out moduleCacheEntry); diff --git a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs index a1bce6c9a27..c6745a4a44c 100644 --- a/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/GetModuleCommand.cs @@ -84,6 +84,18 @@ 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(ParameterSetName = ParameterSet_AvailableLocally)] + [Parameter(ParameterSetName = ParameterSet_AvailableInPsrpSession)] + [Parameter(ParameterSetName = ParameterSet_AvailableInCimSession)] + public SwitchParameter SkipEditionCheck + { + get { return (SwitchParameter)BaseSkipEditionCheck; } + set { BaseSkipEditionCheck = value; } + } + /// /// If specified, then Get-Module refreshes the internal cmdlet analysis cache /// @@ -341,6 +353,18 @@ protected override void ProcessRecord() ThrowTerminatingError(error); } + // -SkipEditionCheck only makes sense for -ListAvailable (otherwise the module is already loaded) + if (SkipEditionCheck && !ListAvailable) + { + ErrorRecord error = new ErrorRecord( + new InvalidOperationException(Modules.SkipEditionCheckNotSupportedWithoutListAvailable), + nameof(Modules.SkipEditionCheckNotSupportedWithoutListAvailable), + ErrorCategory.InvalidOperation, + targetObject: null); + + ThrowTerminatingError(error); + } + var strNames = new List(); if (Name != null) { @@ -423,27 +447,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 +462,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 +473,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 +486,74 @@ 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) + { +#if !UNIX + // Edition check only applies to Windows System32 module path + if (!SkipEditionCheck && ListAvailable && !All) + { + modules = modules.Where(module => module.IsConsideredEditionCompatible); + } +#endif + + if (!String.IsNullOrEmpty(PSEdition)) + { + modules = modules.Where(module => module.CompatiblePSEditions.Contains(PSEdition, StringComparer.OrdinalIgnoreCase)); + } + + if (moduleSpecificationTable != null && moduleSpecificationTable.Count > 0) + { + modules = FilterModulesForSpecificationMatch(modules, moduleSpecificationTable); + } + + return modules; + } + + /// + /// 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 +577,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 3106172ea29..da7926f927e 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 f7d2fc73c22..219f9c9f19d 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, @@ -2376,6 +2391,52 @@ 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; + bool isConsideredCompatible = ModuleUtils.IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions); + if (!BaseSkipEditionCheck && !isConsideredCompatible) + { + 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 (importingModule) + { + 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, + IsConsideredEditionCompatible = false, + }; + } + } + // Process format.ps1xml / types.ps1.xml / RequiredAssemblies // as late as possible, but before ModuleToProcess, ScriptToProcess, NestedModules if (importingModule) @@ -2475,6 +2536,10 @@ internal PSModuleInfo LoadModuleManifest( 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 = isConsideredCompatible; + if (expFeatureList != null) { manifestInfo.ExperimentalFeatures = new ReadOnlyCollection(expFeatureList); @@ -2503,13 +2568,12 @@ internal PSModuleInfo LoadModuleManifest( manifestInfo.AddToModuleList(m); } } + if (compatiblePSEditions != null) { - foreach (var psEdition in compatiblePSEditions) - { - manifestInfo.AddToCompatiblePSEditions(psEdition); - } + manifestInfo.AddToCompatiblePSEditions(compatiblePSEditions); } + if (scriptsToProcess != null) { foreach (var s in scriptsToProcess) @@ -3076,8 +3140,11 @@ internal PSModuleInfo LoadModuleManifest( newManifestInfo.LicenseUri = manifestInfo.LicenseUri; newManifestInfo.IconUri = manifestInfo.IconUri; newManifestInfo.RepositorySourceLocation = manifestInfo.RepositorySourceLocation; + newManifestInfo.IsConsideredEditionCompatible = manifestInfo.IsConsideredEditionCompatible; + newManifestInfo.ExperimentalFeatures = manifestInfo.ExperimentalFeatures; + // If we are in module discovery, then fix the path. if (ss == null) { @@ -3155,10 +3222,7 @@ internal PSModuleInfo LoadModuleManifest( { if (compatiblePSEditions != null) { - foreach (var psEdition in compatiblePSEditions) - { - newManifestInfo.AddToCompatiblePSEditions(psEdition); - } + newManifestInfo.AddToCompatiblePSEditions(compatiblePSEditions); } } diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index d2187f6aca8..f40b4595a38 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -31,6 +31,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; @@ -629,6 +634,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. @@ -923,6 +945,12 @@ private static string SetModulePath() if (!string.IsNullOrEmpty(newModulePathString)) { +#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 + // 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 f4f859aafc2..51153100ec0 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -69,7 +69,7 @@ internal static bool IsPossibleModuleDirectory(string dir) internal static IEnumerable GetAllAvailableModuleFiles(string topDirectoryToCheck) { if (!Directory.Exists(topDirectoryToCheck)) { yield break; } - + var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions; Queue directoriesToCheck = new Queue(); directoriesToCheck.Enqueue(topDirectoryToCheck); @@ -106,6 +106,29 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto } } + /// + /// 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; + } + + return Utils.IsPSEditionSupported(compatiblePSEditions); +#endif + } + internal static IEnumerable GetDefaultAvailableModuleFiles(bool isForAutoDiscovery, ExecutionContext context) { HashSet uniqueModuleFiles = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -344,6 +367,18 @@ internal static bool IsModuleInVersionSubdirectory(string modulePath, out Versio return false; } + internal static bool IsOnSystem32ModulePath(string path) + { +#if UNIX + return false; +#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 /// @@ -371,7 +406,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe { // 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) @@ -387,10 +422,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)) { @@ -398,7 +433,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); @@ -410,7 +445,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"); @@ -427,11 +462,12 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe } string moduleShortName = System.IO.Path.GetFileNameWithoutExtension(modulePath); - var exportedCommands = AnalysisCache.GetExportedCommands(modulePath, false, context); + + 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); @@ -444,10 +480,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 3bf5c0d9a07..2a2534a0f09 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 { @@ -834,6 +837,22 @@ internal void AddToCompatiblePSEditions(string psEdition) _compatiblePSEditions.Add(psEdition); } + internal void AddToCompatiblePSEditions(IEnumerable psEditions) + { + _compatiblePSEditions.AddRange(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. 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; + /// /// ModuleList /// @@ -1585,4 +1604,4 @@ public int GetHashCode(PSModuleInfo obj) } } } -} // System.Management.Automation \ No newline at end of file +} diff --git a/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs b/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs index d34665801f3..e88a28f976c 100644 --- a/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs +++ b/src/System.Management.Automation/engine/Modules/TestModuleManifestCommand.cs @@ -23,6 +23,19 @@ namespace Microsoft.PowerShell.Commands [OutputType(typeof(PSModuleInfo))] public sealed class TestModuleManifestCommand : ModuleCmdletBase { + /// + /// 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; + } + /// /// The output path for the generated file... /// diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index d17e04fd338..5d43d2b6af7 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; + internal static bool ShowMarkdownOutputBypass; /// This member is used for internal test purposes. diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index 11bf9e47769..f88928b3592 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -606,10 +606,16 @@ 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. + A non-empty string value should be specified for an experimental feature declared in the module manifest. 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. + diff --git a/src/System.Management.Automation/utils/PsUtils.cs b/src/System.Management.Automation/utils/PsUtils.cs index 57340056327..f1718854e1c 100644 --- a/src/System.Management.Automation/utils/PsUtils.cs +++ b/src/System.Management.Automation/utils/PsUtils.cs @@ -476,8 +476,18 @@ 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[] ManifestPrivateDataPropertyName = new[] { "PrivateData" }; + internal static readonly string[] FastModuleManifestAnalysisPropertyNames = new[] + { + "AliasesToExport", + "CmdletsToExport", + "CompatiblePSEditions", + "FunctionsToExport", + "NestedModules", + "RootModule", + "ModuleToProcess", + "ModuleVersion" + }; internal static Hashtable GetModuleManifestProperties(string psDataFilePath, string[] keys) { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 new file mode 100644 index 00000000000..3861c1b7c0a --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1 @@ -0,0 +1,862 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$script:oldModulePath = $env:PSModulePath + +function Add-ModulePath +{ + param([string]$Path, [switch]$Prepend) + + $script:oldModulePath = $env:PSModulePath + + if ($Prepend) + { + $env:PSModulePAth = $Path + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + else + { + $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 "," + } +} + +function New-TestNestedModule +{ + param( + [string]$ModuleBase, + [string]$ScriptModuleFilename, + [string]$ScriptModuleContent, + [string]$BinaryModuleFilename, + [string]$BinaryModuleDllPath, + [string]$RootModuleFilename, + [string]$RootModuleContent, + [string[]]$CompatiblePSEditions, + [bool]$UseRootModule, + [bool]$UseAbsolutePath + ) + + $nestedModules = [System.Collections.ArrayList]::new() + + # Create script module + New-Item -Path (Join-Path $ModuleBase $ScriptModuleFileName) -Value $ScriptModuleContent + $nestedModules.Add($ScriptModuleFilename) + + 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) + { + 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 = $nestedModules -join ',' + + $newManifestCmd = "New-ModuleManifest -Path $manifestPath -NestedModules $nestedModules " + if ($CompatiblePSEditions) + { + $compatibleModules = $CompatiblePSEditions -join ',' + $newManifestCmd += "-CompatiblePSEditions $compatibleModules " + } + if ($UseRootModule) + { + $newManifestCmd += "-RootModule $RootModuleFilename " + $newManifestCmd += "-FunctionsToExport @('Test-RootModule') " + } + else + { + $newManifestCmd += "-FunctionsToExport @('Test-ScriptModule') " + } + + $newManifestCmd += "-CmdletsToExport @() -VariablesToExport @() -AliasesToExport @() " + + # Create the manifest + [scriptblock]::Create($newManifestCmd).Invoke() +} + + +Describe "Get-Module with CompatiblePSEditions-checked paths" -Tag "CI" { + + BeforeAll { + if (-not $IsWindows) + { + return + } + + $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 + } + } +} + +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 + } + + 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 + } + + 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 + } +} + +Describe "Get-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 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 }' + + # 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 { + 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) + + 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 = 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 ($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 "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 { + Restore-ModulePath + } + + 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 ` + -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 + + $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 + } + } +} + +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" + } + } +} 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..263f5eacf4d 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 @@ -99,26 +99,35 @@ 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" { - 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 -ErrorAction SilentlyContinue } | Should -Throw -ErrorId 'FormatXmlUpdateException,Microsoft.PowerShell.Commands.ImportModuleCommand' + # 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" + 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" } } 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.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' { 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 }