Enforce the 'CompatiblePSEditions' check for modules from the legacy 'System32' module path (#7183)

- Add `%WINDIR%\System32\WindowsPowerShell\v1.0\Modules` (Windows PowerShell $PSHOME) to the end of the default PSCore 6 module path (i.e. the module path as initially set at startup).
- Cause an error to be thrown by `Import-Module` when a module with `CompatiblePSEditions` not containing `"Core"` is being loaded from the 'System32' module path.
- Suppress output of modules listed by `Get-Module -ListAvailable` from Windows PowerShell $PSHOME when `CompatiblePSEditions` does not contain `"Core"`.
- Introduce the `-SkipCompatibilityCheck` switch parameter on both `Import-Module` and `Get-Module` to respectively allow importing incompatible modules and listing incompatible modules.
- Adds a  `PSEdition` column to the `PSModuleInfo` table view format.
- Ensures that completions are not given for incompatible modules on the System32 module path.
This commit is contained in:
Robert Holt
2018-07-16 19:40:46 -07:00
committed by Travis Plunk
parent da7b5216b5
commit e857135d3c
17 changed files with 1339 additions and 79 deletions
@@ -1196,11 +1196,24 @@ namespace System.Management.Automation.Runspaces
.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());
@@ -398,7 +398,7 @@ namespace System.Management.Automation
#region Module Names
internal static List<CompletionResult> CompleteModuleName(CompletionContext context, bool loadedModulesOnly)
internal static List<CompletionResult> CompleteModuleName(CompletionContext context, bool loadedModulesOnly, bool skipEditionCheck = false)
{
var moduleName = context.WordToComplete ?? string.Empty;
var result = new List<CompletionResult>();
@@ -413,6 +413,12 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
}
}
private static void NativeCompletionModuleCommands(CompletionContext context, string paramName, bool loadedModulesOnly, bool isImportModule, List<CompletionResult> result)
private static void NativeCompletionModuleCommands(
CompletionContext context,
string paramName,
List<CompletionResult> result,
bool loadedModulesOnly = false,
bool isImportModule = false,
bool skipEditionCheck = false)
{
if (string.IsNullOrEmpty(paramName))
{
@@ -3067,7 +3081,7 @@ namespace System.Management.Automation
StringLiterals.PowerShellILAssemblyExtension,
StringLiterals.PowerShellCmdletizationFileExtension
};
var moduleFilesResults = new List<CompletionResult>(CompleteFilename(context, /* containerOnly: */ false, moduleExtensions));
var moduleFilesResults = new List<CompletionResult>(CompleteFilename(context, containerOnly: false, moduleExtensions));
if (moduleFilesResults.Count > 0)
result.AddRange(moduleFilesResults);
@@ -3079,7 +3093,7 @@ namespace System.Management.Automation
}
}
var moduleResults = CompleteModuleName(context, loadedModulesOnly);
var moduleResults = CompleteModuleName(context, loadedModulesOnly, skipEditionCheck);
if (moduleResults != null && moduleResults.Count > 0)
result.AddRange(moduleResults);
@@ -102,6 +102,12 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
return result ?? AnalyzeTheOldWay(modulePath, context, lastWriteTime);
}
/// <summary>
/// Check if a module is compatible with the current PSEdition given its path and its manifest properties.
/// </summary>
/// <param name="modulePath">The path to the module.</param>
/// <param name="moduleManifestProperties">The properties of the module's manifest.</param>
/// <returns></returns>
internal static bool ModuleIsEditionIncompatible(string modulePath, Hashtable moduleManifestProperties)
{
#if UNIX
return false;
#else
if (!ModuleUtils.IsOnSystem32ModulePath(modulePath))
{
return false;
}
if (!moduleManifestProperties.ContainsKey("CompatiblePSEditions"))
{
return true;
}
return !Utils.IsPSEditionSupported(LanguagePrimitives.ConvertTo<string[]>(moduleManifestProperties["CompatiblePSEditions"]));
#endif
}
internal static bool ModuleAnalysisViaGetModuleRequired(object modulePathObj, bool hadCmdlets, bool hadFunctions, bool hadAliases)
{
var modulePath = modulePathObj as string;
@@ -449,6 +480,14 @@ namespace System.Management.Automation
{
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);
@@ -84,6 +84,18 @@ namespace Microsoft.PowerShell.Commands
[ArgumentCompleter(typeof(PSEditionArgumentCompleter))]
public string PSEdition { get; set; }
/// <summary>
/// When set, CompatiblePSEditions checking is disabled for modules in the System32 (Windows PowerShell) module directory.
/// </summary>
[Parameter(ParameterSetName = ParameterSet_AvailableLocally)]
[Parameter(ParameterSetName = ParameterSet_AvailableInPsrpSession)]
[Parameter(ParameterSetName = ParameterSet_AvailableInCimSession)]
public SwitchParameter SkipEditionCheck
{
get { return (SwitchParameter)BaseSkipEditionCheck; }
set { BaseSkipEditionCheck = value; }
}
/// <summary>
/// If specified, then Get-Module refreshes the internal cmdlet analysis cache
/// </summary>
@@ -341,6 +353,18 @@ namespace Microsoft.PowerShell.Commands
ThrowTerminatingError(error);
}
// -SkipEditionCheck only makes sense for -ListAvailable (otherwise the module is already loaded)
if (SkipEditionCheck && !ListAvailable)
{
ErrorRecord error = new ErrorRecord(
new InvalidOperationException(Modules.SkipEditionCheckNotSupportedWithoutListAvailable),
nameof(Modules.SkipEditionCheckNotSupportedWithoutListAvailable),
ErrorCategory.InvalidOperation,
targetObject: null);
ThrowTerminatingError(error);
}
var strNames = new List<string>();
if (Name != null)
{
@@ -423,27 +447,12 @@ namespace Microsoft.PowerShell.Commands
}
}
/// <summary>
/// Determine whether a module info matches a given module specification table and specified PSEdition value.
/// </summary>
/// <param name="moduleInfo"></param>
/// <param name="moduleSpecTable"></param>
/// <param name="edition"></param>
/// <returns></returns>
private static bool ModuleMatch(PSModuleInfo moduleInfo, IDictionary<string, ModuleSpecification> moduleSpecTable, string edition)
{
ModuleSpecification moduleSpecification;
return (String.IsNullOrEmpty(edition) || moduleInfo.CompatiblePSEditions.Contains(edition, StringComparer.OrdinalIgnoreCase)) &&
(!moduleSpecTable.TryGetValue(moduleInfo.Name, out moduleSpecification) || ModuleIntrinsics.IsModuleMatchingModuleSpec(moduleInfo, moduleSpecification));
}
private void GetAvailableViaCimSession(IEnumerable<string> names, IDictionary<string, ModuleSpecification> moduleSpecTable,
CimSession cimSession, Uri resourceUri, string cimNamespace)
{
var remoteModules = GetAvailableViaCimSessionCore(names, cimSession, resourceUri, cimNamespace);
IEnumerable<PSModuleInfo> remoteModules = GetAvailableViaCimSessionCore(names, cimSession, resourceUri, cimNamespace);
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 @@ namespace Microsoft.PowerShell.Commands
private void GetAvailableViaPsrpSession(string[] names, IDictionary<string, ModuleSpecification> moduleSpecTable, PSSession session)
{
var remoteModules = GetAvailableViaPsrpSessionCore(names, session.Runspace);
IEnumerable<PSModuleInfo> remoteModules = GetAvailableViaPsrpSessionCore(names, session.Runspace);
foreach (var remoteModule in remoteModules.Where(remoteModule => ModuleMatch(remoteModule, moduleSpecTable, PSEdition))
)
foreach (PSModuleInfo remoteModule in FilterModulesForEditionAndSpecification(remoteModules, moduleSpecTable))
{
RemoteDiscoveryHelper.AssociatePSModuleInfoWithSession(remoteModule, session);
this.WriteObject(remoteModule);
@@ -465,14 +473,10 @@ namespace Microsoft.PowerShell.Commands
private void GetAvailableLocallyModules(string[] names, IDictionary<string, ModuleSpecification> moduleSpecTable, bool all)
{
var refresh = Refresh.IsPresent;
var modules = GetModule(names, all, refresh);
foreach (
var psModule in
modules.Where(module => ModuleMatch(module, moduleSpecTable, PSEdition)).Select(module => new PSObject(module))
)
IEnumerable<PSModuleInfo> modules = GetModule(names, all, Refresh);
foreach (PSModuleInfo module in FilterModulesForEditionAndSpecification(modules, moduleSpecTable))
{
var psModule = new PSObject(module);
psModule.TypeNames.Insert(0, "ModuleInfoGrouping");
WriteObject(psModule);
}
@@ -482,14 +486,74 @@ namespace Microsoft.PowerShell.Commands
{
var modulesToWrite = Context.Modules.GetModules(names, all);
foreach (
var moduleInfo in
modulesToWrite.Where(moduleInfo => ModuleMatch(moduleInfo, moduleSpecTable, PSEdition))
)
foreach (PSModuleInfo moduleInfo in FilterModulesForEditionAndSpecification(modulesToWrite, moduleSpecTable))
{
WriteObject(moduleInfo);
}
}
/// <summary>
/// Filter an enumeration of PowerShell modules based on the required PowerShell edition
/// and the module specification constraints set for each module (if any).
/// </summary>
/// <param name="modules">The modules to filter through.</param>
/// <param name="moduleSpecificationTable">Module constraints, keyed by module name, to filter modules of that name by.</param>
/// <returns>All modules from the original input that meet both any module edition and module specification constraints provided.</returns>
private IEnumerable<PSModuleInfo> FilterModulesForEditionAndSpecification(
IEnumerable<PSModuleInfo> modules,
IDictionary<string, ModuleSpecification> moduleSpecificationTable)
{
#if !UNIX
// Edition check only applies to Windows System32 module path
if (!SkipEditionCheck && ListAvailable && !All)
{
modules = modules.Where(module => module.IsConsideredEditionCompatible);
}
#endif
if (!String.IsNullOrEmpty(PSEdition))
{
modules = modules.Where(module => module.CompatiblePSEditions.Contains(PSEdition, StringComparer.OrdinalIgnoreCase));
}
if (moduleSpecificationTable != null && moduleSpecificationTable.Count > 0)
{
modules = FilterModulesForSpecificationMatch(modules, moduleSpecificationTable);
}
return modules;
}
/// <summary>
/// Take an enumeration of modules and only return those that match a specification
/// in the given specification table, or have no corresponding entry in the specification table.
/// </summary>
/// <param name="modules">The modules to filter by specification match.</param>
/// <param name="moduleSpecificationTable">The specification lookup table to filter the modules on.</param>
/// <returns>The modules that match their corresponding table entry, or which have no table entry.</returns>
private static IEnumerable<PSModuleInfo> FilterModulesForSpecificationMatch(
IEnumerable<PSModuleInfo> modules,
IDictionary<string, ModuleSpecification> moduleSpecificationTable)
{
Dbg.Assert(moduleSpecificationTable != null, $"Caller to verify that {nameof(moduleSpecificationTable)} is not null");
Dbg.Assert(moduleSpecificationTable.Count != 0, $"Caller to verify that {nameof(moduleSpecificationTable)} is not empty");
foreach (PSModuleInfo module in modules)
{
// No table entry means we return the module
if (!moduleSpecificationTable.TryGetValue(module.Name, out ModuleSpecification moduleSpecification))
{
yield return module;
continue;
}
// Modules with table entries only get returned if they match them
if (ModuleIntrinsics.IsModuleMatchingModuleSpec(module, moduleSpecification))
{
yield return module;
}
}
}
}
/// <summary>
@@ -513,5 +577,4 @@ namespace Microsoft.PowerShell.Commands
}
}
}
} // Microsoft.PowerShell.Commands
}
@@ -207,6 +207,16 @@ namespace Microsoft.PowerShell.Commands
set { BaseForce = value; }
}
/// <summary>
/// Skips the check on CompatiblePSEditions for modules loaded from the System32 module path.
/// </summary>
[Parameter]
public SwitchParameter SkipEditionCheck
{
get { return (SwitchParameter)BaseSkipEditionCheck; }
set { BaseSkipEditionCheck = value; }
}
/// <summary>
/// This parameter causes the session state instance to be written...
/// </summary>
@@ -105,6 +105,11 @@ namespace Microsoft.PowerShell.Commands
/// </summary>
internal bool BaseGlobal { get; set; }
/// <summary>
/// If set, CompatiblePSEditions checking will be disabled for modules on the System32 path.
/// </summary>
internal bool BaseSkipEditionCheck { get; set; }
internal SessionState TargetSessionState
{
get
@@ -256,6 +261,16 @@ namespace Microsoft.PowerShell.Commands
"ModuleVersion"
};
/// <summary>
/// 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.
/// </summary>
internal static IReadOnlyList<string> DefaultCompatiblePSEditions { get; } = new string[]
{
"Desktop"
};
private Dictionary<string, PSModuleInfo> _currentlyProcessingModules = new Dictionary<string, PSModuleInfo>();
internal bool LoadUsingModulePath(bool found, IEnumerable<string> modulePath, string name, SessionState ss,
@@ -2376,6 +2391,52 @@ namespace Microsoft.PowerShell.Commands
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<string> 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 @@ namespace Microsoft.PowerShell.Commands
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<ExperimentalFeature>(expFeatureList);
@@ -2503,13 +2568,12 @@ namespace Microsoft.PowerShell.Commands
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 @@ namespace Microsoft.PowerShell.Commands
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 @@ namespace Microsoft.PowerShell.Commands
{
if (compatiblePSEditions != null)
{
foreach (var psEdition in compatiblePSEditions)
{
newManifestInfo.AddToCompatiblePSEditions(psEdition);
}
newManifestInfo.AddToCompatiblePSEditions(compatiblePSEditions);
}
}
@@ -31,6 +31,11 @@ namespace System.Management.Automation
[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 @@ namespace System.Management.Automation
#endif
}
#if !UNIX
/// <summary>
/// Get the path to the Windows PowerShell module directory under the
/// System32 directory on Windows (the Windows PowerShell $PSHOME).
/// </summary>
/// <returns>The path of the Windows PowerShell system module directory.</returns>
internal static string GetWindowsPowerShellPSHomeModulePath()
{
if (!String.IsNullOrEmpty(InternalTestHooks.TestWindowsPowerShellPSHomeLocation))
{
return InternalTestHooks.TestWindowsPowerShellPSHomeLocation;
}
return s_windowsPowerShellPSHomeModulePath;
}
#endif
/// <summary>
/// Combine the PS system-wide module path and the DSC module path
/// to get the system module paths.
@@ -923,6 +945,12 @@ namespace System.Management.Automation
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);
}
@@ -69,7 +69,7 @@ namespace System.Management.Automation.Internal
internal static IEnumerable<string> GetAllAvailableModuleFiles(string topDirectoryToCheck)
{
if (!Directory.Exists(topDirectoryToCheck)) { yield break; }
var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions;
Queue<string> directoriesToCheck = new Queue<string>();
directoriesToCheck.Enqueue(topDirectoryToCheck);
@@ -106,6 +106,29 @@ namespace System.Management.Automation.Internal
}
}
/// <summary>
/// Check if the CompatiblePSEditions field of a given module
/// declares compatibility with the running PowerShell edition.
/// </summary>
/// <param name="moduleManifestPath">The path to the module manifest being checked.</param>
/// <param name="compatiblePSEditions">The value of the CompatiblePSEditions field of the module manifest.</param>
/// <returns>True if the module is compatible with the running PowerShell edition, false otherwise.</returns>
internal static bool IsPSEditionCompatible(
string moduleManifestPath,
IEnumerable<string> compatiblePSEditions)
{
#if UNIX
return true;
#else
if (!ModuleUtils.IsOnSystem32ModulePath(moduleManifestPath))
{
return true;
}
return Utils.IsPSEditionSupported(compatiblePSEditions);
#endif
}
internal static IEnumerable<string> GetDefaultAvailableModuleFiles(bool isForAutoDiscovery, ExecutionContext context)
{
HashSet<string> uniqueModuleFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -344,6 +367,18 @@ namespace System.Management.Automation.Internal
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
}
/// <summary>
/// Gets a list of matching commands
/// </summary>
@@ -371,7 +406,7 @@ namespace System.Management.Automation.Internal
{
// 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<PSModuleInfo> modules = context.Modules.GetExactMatchModules(moduleName, all: false, exactMatch: true);
PSModuleInfo tempModuleInfo = null;
if (modules.Count != 0)
@@ -387,10 +422,10 @@ namespace System.Management.Automation.Internal
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<string, CommandInfo> entry in psModule.ExportedCommands)
{
if (commandPattern.IsMatch(entry.Value.Name))
{
@@ -398,7 +433,7 @@ namespace System.Management.Automation.Internal
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 @@ namespace System.Management.Automation.Internal
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 @@ namespace System.Management.Automation.Internal
}
string moduleShortName = System.IO.Path.GetFileNameWithoutExtension(modulePath);
var exportedCommands = AnalysisCache.GetExportedCommands(modulePath, false, context);
IDictionary<string, CommandTypes> 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 @@ namespace System.Management.Automation.Internal
tempModuleInfo.SetGuid(ModuleIntrinsics.GetManifestGuid(modulePath));
}
foreach (var pair in exportedCommands)
foreach (KeyValuePair<string, CommandTypes> pair in exportedCommands)
{
var commandName = pair.Key;
var commandTypes = pair.Value;
string commandName = pair.Key;
CommandTypes commandTypes = pair.Value;
if (commandPattern.IsMatch(commandName))
{
@@ -820,7 +820,10 @@ namespace System.Management.Automation
}
/// <summary>
/// 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.
/// </summary>
public IEnumerable<String> CompatiblePSEditions
{
@@ -834,6 +837,22 @@ namespace System.Management.Automation
_compatiblePSEditions.Add(psEdition);
}
internal void AddToCompatiblePSEditions(IEnumerable<string> psEditions)
{
_compatiblePSEditions.AddRange(psEditions);
}
/// <summary>
/// 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.
/// </summary>
internal bool IsConsideredEditionCompatible { get; set; } = true;
/// <summary>
/// ModuleList
/// </summary>
@@ -1585,4 +1604,4 @@ namespace System.Management.Automation
}
}
}
} // System.Management.Automation
}
@@ -23,6 +23,19 @@ namespace Microsoft.PowerShell.Commands
[OutputType(typeof(PSModuleInfo))]
public sealed class TestModuleManifestCommand : ModuleCmdletBase
{
/// <summary>
/// Creates an instance of the Test-ModuleManifest command.
/// </summary>
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;
}
/// <summary>
/// The output path for the generated file...
/// </summary>
@@ -451,7 +451,7 @@ namespace System.Management.Automation
}
/// <summary>
/// Checks whether current monad session supports edition specified
/// Checks whether current PowerShell session supports edition specified
/// by checkEdition.
/// </summary>
/// <param name="checkEdition">Edition to check</param>
@@ -462,7 +462,26 @@ namespace System.Management.Automation
}
/// <summary>
/// Checks whether the specified edition values is allowed.
/// Check whether the current PowerShell session supports any of the specified editions.
/// </summary>
/// <param name="editions">The PowerShell editions to check compatibility with.</param>
/// <returns>True if the edition is supported by this runtime, false otherwise.</returns>
internal static bool IsPSEditionSupported(IEnumerable<string> editions)
{
string currentPSEdition = PSVersionInfo.PSEdition;
foreach (string edition in editions)
{
if (currentPSEdition.Equals(edition, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
/// <summary>
/// Checks whether the specified edition value is allowed.
/// </summary>
/// <param name="editionValue">Edition value to check</param>
/// <returns>true if allowed, false otherwise</returns>
@@ -1449,6 +1468,10 @@ namespace System.Management.Automation.Internal
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;
/// <summary>This member is used for internal test purposes.</summary>
@@ -606,10 +606,16 @@
<data name="PrerequisiteForDesktopEditionOnly" xml:space="preserve">
<value>This prerequisite is valid for the PowerShell Desktop edition only.</value>
</data>
<data name="PSEditionNotSupported" xml:space="preserve">
<value>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.</value>
</data>
<data name="ExperimentalFeatureNameMissingOrEmpty" xml:space="preserve">
<value>A non-empty string value should be specified for an experimental feature declared in the module manifest.</value>
</data>
<data name="InvalidExperimentalFeatureName" xml:space="preserve">
<value>One or more invalid experimental feature names found: {0}. A module experimental feature name should follow this convention: 'ModuleName.FeatureName'.</value>
</data>
<data name="SkipEditionCheckNotSupportedWithoutListAvailable" xml:space="preserve">
<value>The -SkipEditionCheck switch parameter cannot be used without the -ListAvailable switch parameter.</value>
</data>
</root>
@@ -476,8 +476,18 @@ namespace System.Management.Automation
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)
{
@@ -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 <Editions>" -TestCases $successCases -Skip:(-not $IsWindows) {
param($Editions, $ModuleName)
$modules.Name | Should -Contain $ModuleName
}
It "Does not list incompatible modules with -ListAvailable for PSEdition <Editions>" -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 <Editions>" -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 <Editions>" -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 <Editions>" -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 <Editions>" -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 <Editions>" -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 <Editions>" -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 <Editions>" -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 <Editions>" -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 <Editions>" -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 <Editions>" -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: <SkipEditionCheck>, using root module: <UseRootModule>, using absolute path: <UseAbsolutePath>, CompatiblePSEditions: <MarkedEdition>" -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: <SkipEditionCheck>, using root module: <UseRootModule>, using absolute path: <UseAbsolutePath>, CompatiblePSEditions: <MarkedEdition>" -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: <SkipEditionCheck>, using root module: <UseRootModule>, using absolute path: <UseAbsolutePath>, CompatiblePSEditions: <MarkedEdition>" -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: <SkipEditionCheck>, using root module: <UseRootModule>, using absolute path: <UseAbsolutePath>, CompatiblePSEditions: <MarkedEdition>" -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: <SkipEditionCheck>, using root module: <UseRootModule>, using absolute path: <UseAbsolutePath>, CompatiblePSEditions: <MarkedEdition>" -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: <SkipEditionCheck>, using root module: <UseRootModule>, using absolute path: <UseAbsolutePath>, CompatiblePSEditions: <MarkedEdition>" -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"
}
}
}
@@ -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'
}
}
@@ -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' {
@@ -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
}