Report error when PowerShell built-in modules are missing (#16628)

This commit is contained in:
Dongbo Wang
2022-01-12 10:02:03 -08:00
committed by GitHub
parent 1cb3553349
commit cce64f49de
10 changed files with 427 additions and 241 deletions
@@ -376,22 +376,16 @@ namespace System.Management.Automation
foreach (var requiresPSSnapIn in requiresPSSnapIns)
{
IEnumerable<PSSnapInInfo> loadedPSSnapIns = null;
loadedPSSnapIns = context.InitialSessionState.GetPSSnapIn(requiresPSSnapIn.Name);
if (loadedPSSnapIns == null || !loadedPSSnapIns.Any())
var loadedPSSnapIn = context.InitialSessionState.GetPSSnapIn(requiresPSSnapIn.Name);
if (loadedPSSnapIn is null)
{
if (requiresMissingPSSnapIns == null)
{
requiresMissingPSSnapIns = new Collection<string>();
}
requiresMissingPSSnapIns ??= new Collection<string>();
requiresMissingPSSnapIns.Add(BuildPSSnapInDisplayName(requiresPSSnapIn));
}
else
{
// the requires PSSnapin is loaded. now check the PSSnapin version
PSSnapInInfo loadedPSSnapIn = loadedPSSnapIns.First();
Diagnostics.Assert(loadedPSSnapIn.Version != null,
Dbg.Assert(loadedPSSnapIn.Version != null,
string.Format(
CultureInfo.InvariantCulture,
"Version is null for loaded PSSnapin {0}.", loadedPSSnapIn));
@@ -400,11 +394,7 @@ namespace System.Management.Automation
if (!AreInstalledRequiresVersionsCompatible(
requiresPSSnapIn.Version, loadedPSSnapIn.Version))
{
if (requiresMissingPSSnapIns == null)
{
requiresMissingPSSnapIns = new Collection<string>();
}
requiresMissingPSSnapIns ??= new Collection<string>();
requiresMissingPSSnapIns.Add(BuildPSSnapInDisplayName(requiresPSSnapIn));
}
}
@@ -1063,14 +1053,12 @@ namespace System.Management.Automation
return null;
CmdletInfo cmdletInfo = context.SessionState.InvokeCommand.GetCmdlet("Microsoft.PowerShell.Core\\Get-Module");
if ((commandOrigin == CommandOrigin.Internal) ||
((cmdletInfo != null) && (cmdletInfo.Visibility == SessionStateEntryVisibility.Public)))
if (commandOrigin == CommandOrigin.Internal || cmdletInfo?.Visibility == SessionStateEntryVisibility.Public)
{
// Search for a module with a matching command, as long as the user would have the ability to
// import the module.
cmdletInfo = context.SessionState.InvokeCommand.GetCmdlet("Microsoft.PowerShell.Core\\Import-Module");
if (((commandOrigin == CommandOrigin.Internal) ||
((cmdletInfo != null) && (cmdletInfo.Visibility == SessionStateEntryVisibility.Public))))
if (commandOrigin == CommandOrigin.Internal || cmdletInfo?.Visibility == SessionStateEntryVisibility.Public)
{
discoveryTracer.WriteLine("Executing non module-qualified search: {0}", commandName);
context.CommandDiscovery.RegisterLookupCommandInfoAction("ActiveModuleSearch", commandName);
@@ -1085,30 +1073,33 @@ namespace System.Management.Automation
{
// WinBlue:69141 - We need to get the full path here because the module path might be C:\Users\User1\DOCUME~1
// While the exportedCommands are cached, they are cached with the full path
string expandedModulePath = IO.Path.GetFullPath(modulePath);
string moduleShortName = System.IO.Path.GetFileNameWithoutExtension(expandedModulePath);
string expandedModulePath = Path.GetFullPath(modulePath);
string moduleShortName = Path.GetFileNameWithoutExtension(expandedModulePath);
var exportedCommands = AnalysisCache.GetExportedCommands(expandedModulePath, false, context);
if (exportedCommands == null) { continue; }
CommandTypes exportedCommandTypes;
// Skip if module only has class or other types and no commands.
if (exportedCommands.TryGetValue(commandName, out exportedCommandTypes))
if (exportedCommands.TryGetValue(commandName, out CommandTypes exportedCommandTypes))
{
Exception exception;
discoveryTracer.WriteLine("Found in module: {0}", expandedModulePath);
Collection<PSModuleInfo> matchingModule = AutoloadSpecifiedModule(expandedModulePath, context,
Collection<PSModuleInfo> matchingModule = AutoloadSpecifiedModule(
expandedModulePath,
context,
cmdletInfo != null ? cmdletInfo.Visibility : SessionStateEntryVisibility.Private,
out exception);
lastError = exception;
if ((matchingModule == null) || (matchingModule.Count == 0))
out lastError);
if (matchingModule is null || matchingModule.Count == 0)
{
string error = StringUtil.Format(DiscoveryExceptions.CouldNotAutoImportMatchingModule, commandName, moduleShortName);
CommandNotFoundException commandNotFound = new CommandNotFoundException(
string errorMessage = lastError is null
? StringUtil.Format(DiscoveryExceptions.CouldNotAutoImportMatchingModule, commandName, moduleShortName)
: StringUtil.Format(DiscoveryExceptions.CouldNotAutoImportMatchingModuleWithErrorMessage, commandName, moduleShortName, lastError.Message);
throw new CommandNotFoundException(
originalCommandName,
lastError,
"CouldNotAutoloadMatchingModule", error);
throw commandNotFound;
"CouldNotAutoloadMatchingModule",
errorMessage);
}
result = LookupCommandInfo(commandName, commandTypes, searchResolutionOptions, commandOrigin, context);
@@ -1663,11 +1663,6 @@ namespace System.Management.Automation.Runspaces
ss.DisableFormatUpdates = this.DisableFormatUpdates;
foreach (var s in this.defaultSnapins)
{
ss.defaultSnapins.Add(s);
}
foreach (var s in ImportedSnapins)
{
ss.ImportedSnapins.Add(s.Key, s.Value);
@@ -3801,34 +3796,26 @@ namespace System.Management.Automation.Runspaces
// Now actually load the snapin...
PSSnapInInfo snapin = ImportPSSnapIn(newPSSnapIn, out warning);
if (snapin != null)
{
ImportedSnapins.Add(snapin.Name, snapin);
}
return snapin;
}
internal PSSnapInInfo ImportCorePSSnapIn()
{
// Load Microsoft.PowerShell.Core as a snapin
// Load Microsoft.PowerShell.Core as a snapin.
PSSnapInInfo coreSnapin = PSSnapInReader.ReadCoreEngineSnapIn();
this.defaultSnapins.Add(coreSnapin);
try
{
PSSnapInException warning;
this.ImportPSSnapIn(coreSnapin, out warning);
}
catch (PSSnapInException)
{
throw;
}
ImportPSSnapIn(coreSnapin, out _);
return coreSnapin;
}
internal PSSnapInInfo ImportPSSnapIn(PSSnapInInfo psSnapInInfo, out PSSnapInException warning)
{
if (psSnapInInfo == null)
{
ArgumentNullException e = new ArgumentNullException(nameof(psSnapInInfo));
throw e;
}
// See if the snapin is already loaded. If has been then there will be an entry in the
// Assemblies list for it already...
bool reload = true;
@@ -3861,12 +3848,6 @@ namespace System.Management.Automation.Runspaces
Dictionary<string, List<SessionStateAliasEntry>> aliases = null;
Dictionary<string, SessionStateProviderEntry> providers = null;
if (psSnapInInfo == null)
{
ArgumentNullException e = new ArgumentNullException(nameof(psSnapInInfo));
throw e;
}
Assembly assembly = null;
string helpFile = null;
@@ -3985,37 +3966,18 @@ namespace System.Management.Automation.Runspaces
}
}
ImportedSnapins.Add(psSnapInInfo.Name, psSnapInInfo);
return psSnapInInfo;
}
internal List<PSSnapInInfo> GetPSSnapIn(string psSnapinName)
internal PSSnapInInfo GetPSSnapIn(string psSnapinName)
{
List<PSSnapInInfo> loadedSnapins = null;
foreach (var defaultSnapin in defaultSnapins)
if (ImportedSnapins.TryGetValue(psSnapinName, out PSSnapInInfo importedSnapin))
{
if (defaultSnapin.Name.Equals(psSnapinName, StringComparison.OrdinalIgnoreCase))
{
if (loadedSnapins == null)
{
loadedSnapins = new List<PSSnapInInfo>();
}
loadedSnapins.Add(defaultSnapin);
}
return importedSnapin;
}
PSSnapInInfo importedSnapin = null;
if (ImportedSnapins.TryGetValue(psSnapinName, out importedSnapin))
{
if (loadedSnapins == null)
{
loadedSnapins = new List<PSSnapInInfo>();
}
loadedSnapins.Add(importedSnapin);
}
return loadedSnapins;
return null;
}
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")]
@@ -4895,7 +4857,6 @@ end {
internal static readonly string CoreSnapin = "Microsoft.PowerShell.Core";
internal static readonly string CoreModule = "Microsoft.PowerShell.Core";
internal Collection<PSSnapInInfo> defaultSnapins = new Collection<PSSnapInInfo>();
// The list of engine modules to create warnings when you try to remove them
internal static readonly HashSet<string> EngineModules = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
@@ -636,6 +636,8 @@ namespace Microsoft.PowerShell.Commands
private PSModuleInfo ImportModule_LocallyViaName(ImportModuleOptions importModuleOptions, string name)
{
bool shallWriteError = !importModuleOptions.SkipSystem32ModulesAndSuppressError;
try
{
bool found = false;
@@ -671,14 +673,14 @@ namespace Microsoft.PowerShell.Commands
}
bool alreadyLoaded = false;
var manifestProcessingFlags = ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.NullOnFirstError;
if (shallWriteError)
{
manifestProcessingFlags |= ManifestProcessingFlags.WriteErrors;
}
if (!string.IsNullOrEmpty(rootedPath))
{
// TODO/FIXME: use IsModuleAlreadyLoaded to get consistent behavior
// TODO/FIXME: (for example checking ModuleType != Manifest below seems incorrect - cdxml modules also declare their own version)
// PSModuleInfo alreadyLoadedModule = null;
// TryGetFromModuleTable(rootedPath, out alreadyLoadedModule);
// if (!BaseForce && IsModuleAlreadyLoaded(alreadyLoadedModule))
// If the module has already been loaded, just emit it and continue...
if (!BaseForce && TryGetFromModuleTable(rootedPath, out PSModuleInfo module))
{
@@ -725,9 +727,14 @@ namespace Microsoft.PowerShell.Commands
RemoveModule(moduleToRemove);
}
foundModule = LoadModule(rootedPath, null, this.BasePrefix, null, ref importModuleOptions,
ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors | ManifestProcessingFlags.NullOnFirstError,
out found);
foundModule = LoadModule(
fileName: rootedPath,
moduleBase: null,
prefix: BasePrefix,
ss: null, /*SessionState*/
ref importModuleOptions,
manifestProcessingFlags,
out found);
}
else if (Directory.Exists(rootedPath))
{
@@ -738,21 +745,24 @@ namespace Microsoft.PowerShell.Commands
}
// Load the latest valid version if it is a multi-version module directory
foundModule = LoadUsingMultiVersionModuleBase(rootedPath,
ManifestProcessingFlags.LoadElements |
ManifestProcessingFlags.WriteErrors |
ManifestProcessingFlags.NullOnFirstError,
importModuleOptions, out found);
foundModule = LoadUsingMultiVersionModuleBase(rootedPath, manifestProcessingFlags, importModuleOptions, out found);
if (!found)
{
// If the path is a directory, double up the end of the string
// then try to load that using extensions...
rootedPath = Path.Combine(rootedPath, Path.GetFileName(rootedPath));
foundModule = LoadUsingExtensions(null, rootedPath, rootedPath, null, null, this.BasePrefix, /*SessionState*/ null,
importModuleOptions,
ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors | ManifestProcessingFlags.NullOnFirstError,
out found);
foundModule = LoadUsingExtensions(
parentModule: null,
moduleName: rootedPath,
fileBaseName: rootedPath,
extension: null,
moduleBase: null,
prefix: BasePrefix,
ss: null, /*SessionState*/
importModuleOptions,
manifestProcessingFlags,
out found);
}
}
}
@@ -762,7 +772,7 @@ namespace Microsoft.PowerShell.Commands
// Check if module could be a snapin. This was the case for PowerShell version 2 engine modules.
if (InitialSessionState.IsEngineModule(name))
{
PSSnapInInfo snapin = ModuleCmdletBase.GetEngineSnapIn(Context, name);
PSSnapInInfo snapin = Context.CurrentRunspace.InitialSessionState.GetPSSnapIn(name);
// Return the command if we found a module
if (snapin != null)
@@ -786,16 +796,28 @@ namespace Microsoft.PowerShell.Commands
// If there is no extension, we'll have to search using the extensions
if (!string.IsNullOrEmpty(Path.GetExtension(name)))
{
foundModule = LoadModule(name, null, this.BasePrefix, null, ref importModuleOptions,
ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors | ManifestProcessingFlags.NullOnFirstError,
out found);
foundModule = LoadModule(
fileName: name,
moduleBase: null,
prefix: BasePrefix,
ss: null, /*SessionState*/
ref importModuleOptions,
manifestProcessingFlags,
out found);
}
else
{
foundModule = LoadUsingExtensions(null, name, name, null, null, this.BasePrefix, /*SessionState*/ null,
importModuleOptions,
ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors | ManifestProcessingFlags.NullOnFirstError,
out found);
foundModule = LoadUsingExtensions(
parentModule: null,
moduleName: name,
fileBaseName: name,
extension: null,
moduleBase: null,
prefix: BasePrefix,
ss: null, /*SessionState*/
importModuleOptions,
manifestProcessingFlags,
out found);
}
}
else
@@ -807,14 +829,17 @@ namespace Microsoft.PowerShell.Commands
this.AddToAppDomainLevelCache = true;
}
found = LoadUsingModulePath(found, modulePath, name, /* SessionState*/ null,
importModuleOptions,
ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors | ManifestProcessingFlags.NullOnFirstError,
out foundModule);
found = LoadUsingModulePath(
modulePath,
name,
ss: null, /* SessionState*/
importModuleOptions,
manifestProcessingFlags,
out foundModule);
}
}
if (!found)
if (!found && shallWriteError)
{
ErrorRecord er = null;
string message = null;
@@ -856,8 +881,10 @@ namespace Microsoft.PowerShell.Commands
}
catch (PSInvalidOperationException e)
{
ErrorRecord er = new ErrorRecord(e.ErrorRecord, e);
WriteError(er);
if (shallWriteError)
{
WriteError(new ErrorRecord(e.ErrorRecord, e));
}
}
return null;
@@ -1945,27 +1972,26 @@ namespace Microsoft.PowerShell.Commands
return match;
}
private List<T> FilterModuleCollection<T>(IEnumerable<T> moduleCollection)
private IEnumerable<T> FilterModuleCollection<T>(IEnumerable<T> moduleCollection)
{
List<T> filteredModuleCollection = null;
if (moduleCollection != null)
if (moduleCollection is null)
{
// the ModuleDeny list is cached in PowerShellConfig object
string[] moduleDenyList = PowerShellConfig.Instance.GetWindowsPowerShellCompatibilityModuleDenyList();
if (moduleDenyList?.Any() != true)
return null;
}
// The ModuleDeny list is cached in PowerShellConfig object
string[] moduleDenyList = PowerShellConfig.Instance.GetWindowsPowerShellCompatibilityModuleDenyList();
if (moduleDenyList is null || moduleDenyList.Length == 0)
{
return moduleCollection;
}
var filteredModuleCollection = new List<T>();
foreach (var module in moduleCollection)
{
if (!IsModuleInDenyList(moduleDenyList, module as string, module as ModuleSpecification))
{
filteredModuleCollection = new List<T>(moduleCollection);
}
else
{
filteredModuleCollection = new List<T>();
foreach (var module in moduleCollection)
{
if (!IsModuleInDenyList(moduleDenyList, module as string, module as ModuleSpecification))
{
filteredModuleCollection.Add(module);
}
}
filteredModuleCollection.Add(module);
}
}
@@ -1977,38 +2003,73 @@ namespace Microsoft.PowerShell.Commands
Debug.Assert(string.IsNullOrEmpty(moduleName) ^ (moduleSpec == null), "Either moduleName or moduleSpec must be specified");
// moduleName can be just a module name and it also can be a full path to psd1 from which we need to extract the module name
string coreModuleToLoad = ModuleIntrinsics.GetModuleName(moduleSpec == null ? moduleName : moduleSpec.Name);
string moduleToLoad = ModuleIntrinsics.GetModuleName(moduleSpec is null ? moduleName : moduleSpec.Name);
var isModuleToLoadEngineModule = InitialSessionState.IsEngineModule(coreModuleToLoad);
string[] noClobberModuleList = PowerShellConfig.Instance.GetWindowsPowerShellCompatibilityNoClobberModuleList();
if (isModuleToLoadEngineModule || ((noClobberModuleList != null) && noClobberModuleList.Contains(coreModuleToLoad, StringComparer.OrdinalIgnoreCase)))
var isBuiltInModule = BuiltInModules.TryGetValue(moduleToLoad, out string normalizedName);
if (isBuiltInModule)
{
// if it is one of engine modules - first try to load it from $PSHOME\Modules
// otherwise rely on $env:PSModulePath (in which WinPS module location has to go after CorePS module location)
if (isModuleToLoadEngineModule)
moduleToLoad = normalizedName;
}
string[] noClobberModuleList = PowerShellConfig.Instance.GetWindowsPowerShellCompatibilityNoClobberModuleList();
if (isBuiltInModule || noClobberModuleList?.Contains(moduleToLoad, StringComparer.OrdinalIgnoreCase) == true)
{
bool shouldLoadModuleLocally = true;
if (isBuiltInModule)
{
string expectedCoreModulePath = Path.Combine(ModuleIntrinsics.GetPSHomeModulePath(), coreModuleToLoad);
if (Directory.Exists(expectedCoreModulePath))
PSSnapInInfo loadedSnapin = Context.CurrentRunspace.InitialSessionState.GetPSSnapIn(moduleToLoad);
shouldLoadModuleLocally = loadedSnapin is null;
if (shouldLoadModuleLocally)
{
coreModuleToLoad = expectedCoreModulePath;
// If it is one of built-in modules, first try loading it from $PSHOME\Modules, otherwise rely on $env:PSModulePath.
string expectedCoreModulePath = Path.Combine(ModuleIntrinsics.GetPSHomeModulePath(), moduleToLoad);
if (Directory.Exists(expectedCoreModulePath))
{
moduleToLoad = expectedCoreModulePath;
}
}
}
if (moduleSpec == null)
if (shouldLoadModuleLocally)
{
ImportModule_LocallyViaName_WithTelemetry(importModuleOptions, coreModuleToLoad);
}
else
{
ModuleSpecification tmpModuleSpec = new ModuleSpecification()
// Here we want to load a core-edition compatible version of the module, so the loading procedure will skip
// the 'System32' module path when searching. Also, we want to suppress writing out errors in case that a
// core-compatible version of the module cannot be found, because:
// 1. that's OK as long as it's not a PowerShell built-in module such as the 'Utility' moudle;
// 2. the error message will be confusing to the user.
bool savedValue = importModuleOptions.SkipSystem32ModulesAndSuppressError;
importModuleOptions.SkipSystem32ModulesAndSuppressError = true;
PSModuleInfo moduleInfo = moduleSpec is null
? ImportModule_LocallyViaName_WithTelemetry(importModuleOptions, moduleToLoad)
: ImportModule_LocallyViaFQName(
importModuleOptions,
new ModuleSpecification()
{
Guid = moduleSpec.Guid,
MaximumVersion = moduleSpec.MaximumVersion,
Version = moduleSpec.Version,
RequiredVersion = moduleSpec.RequiredVersion,
Name = moduleToLoad
});
// If we failed to load a core-compatible version of a built-in module, we should stop trying to load the
// module in 'WinCompat' mode and report an error. This could happen when a user didn't correctly deploy
// the built-in modules, which would result in very confusing errors when the module auto-loading silently
// attempts to load those built-in modules in 'WinCompat' mode from the 'System32' module path.
//
// If the loading failed but it's NOT a built-in module, then it's fine to ignore this failure and continue
// to load the module in 'WinCompat' mode.
if (moduleInfo is null && isBuiltInModule)
{
Guid = moduleSpec.Guid,
MaximumVersion = moduleSpec.MaximumVersion,
Version = moduleSpec.Version,
RequiredVersion = moduleSpec.RequiredVersion,
Name = coreModuleToLoad
};
ImportModule_LocallyViaFQName(importModuleOptions, tmpModuleSpec);
throw new InvalidOperationException(
StringUtil.Format(
Modules.CannotFindCoreCompatibleBuiltInModule,
moduleToLoad));
}
importModuleOptions.SkipSystem32ModulesAndSuppressError = savedValue;
}
importModuleOptions.NoClobberExportPSSession = true;
@@ -2020,28 +2081,15 @@ namespace Microsoft.PowerShell.Commands
IList<PSModuleInfo> moduleProxyList = new List<PSModuleInfo>();
#if !UNIX
// one of the two parameters can be passed: either ModuleNames (most of the time) or ModuleSpecifications (they are used in different parameter sets)
List<string> filteredModuleNames = FilterModuleCollection(moduleNames);
List<ModuleSpecification> filteredModuleFullyQualifiedNames = FilterModuleCollection(moduleFullyQualifiedNames);
IEnumerable<string> filteredModuleNames = FilterModuleCollection(moduleNames);
IEnumerable<ModuleSpecification> filteredModuleFullyQualifiedNames = FilterModuleCollection(moduleFullyQualifiedNames);
// do not setup WinCompat resources if we have no modules to import
if ((filteredModuleNames?.Any() != true) && (filteredModuleFullyQualifiedNames?.Any() != true))
if (filteredModuleNames?.Any() != true && filteredModuleFullyQualifiedNames?.Any() != true)
{
return moduleProxyList;
}
var winPSVersionString = Utils.GetWindowsPowerShellVersionFromRegistry();
if (!winPSVersionString.StartsWith("5.1", StringComparison.OrdinalIgnoreCase))
{
string errorMessage = string.Format(CultureInfo.InvariantCulture, Modules.WinCompatRequredVersionError, winPSVersionString);
throw new InvalidOperationException(errorMessage);
}
PSSession WindowsPowerShellCompatRemotingSession = CreateWindowsPowerShellCompatResources();
if (WindowsPowerShellCompatRemotingSession == null)
{
return new List<PSModuleInfo>();
}
// perform necessary preparations if module has to be imported with NoClobber mode
if (filteredModuleNames != null)
{
@@ -2059,13 +2107,26 @@ namespace Microsoft.PowerShell.Commands
}
}
var winPSVersionString = Utils.GetWindowsPowerShellVersionFromRegistry();
if (!winPSVersionString.StartsWith("5.1", StringComparison.OrdinalIgnoreCase))
{
string errorMessage = string.Format(CultureInfo.InvariantCulture, Modules.WinCompatRequredVersionError, winPSVersionString);
throw new InvalidOperationException(errorMessage);
}
PSSession WindowsPowerShellCompatRemotingSession = CreateWindowsPowerShellCompatResources();
if (WindowsPowerShellCompatRemotingSession == null)
{
return new List<PSModuleInfo>();
}
// perform the module import / proxy generation
moduleProxyList = ImportModule_RemotelyViaPsrpSession(importModuleOptions, filteredModuleNames, filteredModuleFullyQualifiedNames, WindowsPowerShellCompatRemotingSession, usingWinCompat: true);
foreach (PSModuleInfo moduleProxy in moduleProxyList)
{
moduleProxy.IsWindowsPowerShellCompatModule = true;
System.Threading.Interlocked.Increment(ref s_WindowsPowerShellCompatUsageCounter);
Interlocked.Increment(ref s_WindowsPowerShellCompatUsageCounter);
string message = StringUtil.Format(Modules.WinCompatModuleWarning, moduleProxy.Name, WindowsPowerShellCompatRemotingSession.Name);
WriteWarning(message);
@@ -105,6 +105,12 @@ namespace Microsoft.PowerShell.Commands
/// Historically -AllowClobber in these scenarios was set as True.
/// </summary>
internal bool NoClobberExportPSSession;
/// <summary>
/// Flag that controls skipping the System32 module path when searching a module in module paths. It also suppresses
/// writing out errors when specified.
/// </summary>
internal bool SkipSystem32ModulesAndSuppressError;
}
/// <summary>
@@ -280,6 +286,21 @@ namespace Microsoft.PowerShell.Commands
"ModuleVersion"
};
/// <summary>
/// List of PowerShell built-in modules that are shipped with PowerShell only, not on PS Gallery.
/// </summary>
protected static readonly HashSet<string> BuiltInModules = new(StringComparer.OrdinalIgnoreCase)
{
"CimCmdlets",
"Microsoft.PowerShell.Diagnostics",
"Microsoft.PowerShell.Host",
"Microsoft.PowerShell.Management",
"Microsoft.PowerShell.Security",
"Microsoft.PowerShell.Utility",
"Microsoft.WSMan.Management",
"PSDiagnostics",
};
/// <summary>
/// When module manifests lack a CompatiblePSEditions field,
/// they will be treated as if they have this value.
@@ -307,14 +328,25 @@ namespace Microsoft.PowerShell.Commands
private readonly Dictionary<string, PSModuleInfo> _currentlyProcessingModules = new Dictionary<string, PSModuleInfo>();
internal bool LoadUsingModulePath(bool found, IEnumerable<string> modulePath, string name, SessionState ss,
ImportModuleOptions options, ManifestProcessingFlags manifestProcessingFlags, out PSModuleInfo module)
internal bool LoadUsingModulePath(
IEnumerable<string> modulePath,
string name,
SessionState ss,
ImportModuleOptions options,
ManifestProcessingFlags manifestProcessingFlags,
out PSModuleInfo module)
{
return LoadUsingModulePath(null, found, modulePath, name, ss, options, manifestProcessingFlags, out module);
return LoadUsingModulePath(parentModule: null, modulePath, name, ss, options, manifestProcessingFlags, out module);
}
internal bool LoadUsingModulePath(PSModuleInfo parentModule, bool found, IEnumerable<string> modulePath, string name, SessionState ss,
ImportModuleOptions options, ManifestProcessingFlags manifestProcessingFlags, out PSModuleInfo module)
internal bool LoadUsingModulePath(
PSModuleInfo parentModule,
IEnumerable<string> modulePath,
string name,
SessionState ss,
ImportModuleOptions options,
ManifestProcessingFlags manifestProcessingFlags,
out PSModuleInfo module)
{
string extension = Path.GetExtension(name);
string fileBaseName;
@@ -325,11 +357,18 @@ namespace Microsoft.PowerShell.Commands
extension = null;
}
else
{
fileBaseName = name.Substring(0, name.Length - extension.Length);
}
// Now search using the module path...
bool found = false;
foreach (string path in modulePath)
{
if (options.SkipSystem32ModulesAndSuppressError && ModuleUtils.IsOnSystem32ModulePath(path))
{
continue;
}
#if UNIX
foreach (string folder in Directory.EnumerateDirectories(path))
{
@@ -823,7 +862,7 @@ namespace Microsoft.PowerShell.Commands
}
// Otherwise try the module path
found = LoadUsingModulePath(parentModule, found, modulePath,
found = LoadUsingModulePath(parentModule, modulePath,
moduleSpecification.Name, ss,
options, manifestProcessingFlags, out module);
}
@@ -2375,17 +2414,13 @@ namespace Microsoft.PowerShell.Commands
{
if (importingModule)
{
IList<PSModuleInfo> moduleProxies = ImportModulesUsingWinCompat(new string[] { moduleManifestPath }, null, options);
IList<PSModuleInfo> moduleProxies = ImportModulesUsingWinCompat(
moduleNames: new string[] { moduleManifestPath },
moduleFullyQualifiedNames: null,
importModuleOptions: options);
// we are loading by a single ManifestPath so expect max of 1
if (moduleProxies.Count > 0)
{
return moduleProxies[0];
}
else
{
return null;
}
// We are loading by a single ManifestPath so expect max of 1
return moduleProxies.Count > 0 ? moduleProxies[0] : null;
}
}
else
@@ -3706,7 +3741,7 @@ namespace Microsoft.PowerShell.Commands
// If the RequiredModule is one of the Engine modules, then they could have been loaded as snapins (using InitialSessionState.CreateDefault())
if (result == null && InitialSessionState.IsEngineModule(requiredModule.Name))
{
result = ModuleCmdletBase.GetEngineSnapIn(context, requiredModule.Name);
result = context.CurrentRunspace.InitialSessionState.GetPSSnapIn(requiredModule.Name);
if (result != null)
{
loaded = true;
@@ -4883,7 +4918,7 @@ namespace Microsoft.PowerShell.Commands
}
}
internal static System.EventHandler<LocationChangedEventArgs> SyncCurrentLocationDelegate;
internal static EventHandler<LocationChangedEventArgs> SyncCurrentLocationDelegate;
internal virtual IList<PSModuleInfo> ImportModulesUsingWinCompat(IEnumerable<string> moduleNames, IEnumerable<ModuleSpecification> moduleFullyQualifiedNames, ImportModuleOptions importModuleOptions) { throw new System.NotImplementedException(); }
@@ -5309,9 +5344,8 @@ namespace Microsoft.PowerShell.Commands
string moduleName, string fileBaseName, string extension, string moduleBase,
string prefix, SessionState ss, ImportModuleOptions options, ManifestProcessingFlags manifestProcessingFlags, out bool found)
{
bool throwAwayModuleFileFound = false;
return LoadUsingExtensions(parentModule, moduleName, fileBaseName, extension, moduleBase, prefix, ss,
options, manifestProcessingFlags, out found, out throwAwayModuleFileFound);
options, manifestProcessingFlags, out found, out _);
}
/// <summary>
@@ -5412,7 +5446,6 @@ namespace Microsoft.PowerShell.Commands
}
else if (File.Exists(fileName))
{
moduleFileFound = true;
// Win8: 325243 - Added the version check so that we do not unload modules with the same name but different version
if (BaseForce && DoesAlreadyLoadedModuleSatisfyConstraints(module))
{
@@ -7359,29 +7392,6 @@ namespace Microsoft.PowerShell.Commands
}
}
/// <summary>
/// Search a PSSnapin with the specified name.
/// </summary>
internal static PSSnapInInfo GetEngineSnapIn(ExecutionContext context, string name)
{
HashSet<PSSnapInInfo> snapinSet = new HashSet<PSSnapInInfo>();
List<CmdletInfo> cmdlets = context.SessionState.InvokeCommand.GetCmdlets();
foreach (CmdletInfo cmdlet in cmdlets)
{
PSSnapInInfo snapin = cmdlet.PSSnapIn;
if (snapin != null && !snapinSet.Contains(snapin))
snapinSet.Add(snapin);
}
foreach (PSSnapInInfo snapin in snapinSet)
{
if (string.Equals(snapin.Name, name, StringComparison.OrdinalIgnoreCase))
return snapin;
}
return null;
}
/// <summary>
/// Returns the context cached ModuleTable module for import only if found and has safe language boundaries while
/// exporting all functions by default.
@@ -976,20 +976,17 @@ namespace System.Management.Automation
try
{
string psHome = Utils.DefaultPowerShellAppBase;
if (!string.IsNullOrEmpty(psHome))
{
// Win8: 584267 Powershell Modules are listed twice in x86, and cannot be removed
// This happens because ModuleTable uses Path as the key and CBS installer
// expands the path to include "SysWOW64" (for
// HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\PowerShell\3\PowerShellEngine ApplicationBase).
// Because of this, the module that is getting loaded during startup (through LocalRunspace)
// is using "SysWow64" in the key. Later, when Import-Module is called, it loads the
// module using ""System32" in the key.
#if !UNIX
psHome = psHome.ToLowerInvariant().Replace("\\syswow64\\", "\\system32\\");
// Win8: 584267 Powershell Modules are listed twice in x86, and cannot be removed.
// This happens because 'ModuleTable' uses Path as the key and x86 WinPS has "SysWOW64" in its $PSHOME.
// Because of this, the module that is getting loaded during startup (through LocalRunspace) is using
// "SysWow64" in the key. Later, when 'Import-Module' is called, it loads the module using ""System32"
// in the key.
// For the cross-platform PowerShell, a user can choose to install it under "C:\Windows\SysWOW64", and
// thus it may have the same problem as described above. So we keep this line of code.
psHome = psHome.ToLowerInvariant().Replace(@"\syswow64\", @"\system32\");
#endif
Interlocked.CompareExchange(ref s_psHomeModulePath, Path.Combine(psHome, "Modules"), null);
}
Interlocked.CompareExchange(ref s_psHomeModulePath, Path.Combine(psHome, "Modules"), null);
}
catch (System.Security.SecurityException)
{
@@ -121,7 +121,7 @@ namespace System.Management.Automation.Internal
#if UNIX
return true;
#else
if (!ModuleUtils.IsOnSystem32ModulePath(moduleManifestPath))
if (!IsOnSystem32ModulePath(moduleManifestPath))
{
return true;
}
@@ -221,11 +221,7 @@ namespace System.Management.Automation
{
get
{
#pragma warning disable 56503
throw PSTraceSource.NewNotImplementedException();
#pragma warning restore 56503
}
}
@@ -236,11 +232,7 @@ namespace System.Management.Automation
{
get
{
#pragma warning disable 56503
throw PSTraceSource.NewNotImplementedException();
#pragma warning restore 56503
}
}
@@ -206,6 +206,10 @@ The #requires statement must be in one of the following formats:
<data name="CouldNotAutoImportMatchingModule" xml:space="preserve">
<value>The '{0}' command was found in the module '{1}', but the module could not be loaded. For more information, run 'Import-Module {1}'.</value>
</data>
<data name="CouldNotAutoImportMatchingModuleWithErrorMessage" xml:space="preserve">
<value>The '{0}' command was found in the module '{1}', but the module could not be loaded due to the following error: [{2}]
For more information, run 'Import-Module {1}'.</value>
</data>
<data name="CouldNotAutoImportModule" xml:space="preserve">
<value>The module '{0}' could not be loaded. For more information, run 'Import-Module {0}'.</value>
</data>
@@ -624,4 +624,7 @@
<data name="CannotCreateModuleWithScriptBlock" xml:space="preserve">
<value>Cannot create new module while the session is in ConstrainedLanguage mode.</value>
</data>
<data name="CannotFindCoreCompatibleBuiltInModule" xml:space="preserve">
<value>Cannot find the built-in module '{0}' that is compatible with the 'Core' edition. Please make sure the PowerShell built-in modules are available. They usually come with the PowerShell package under the $PSHOME module path, and are required for PowerShell to function properly.</value>
</data>
</root>
@@ -11,7 +11,7 @@ function Add-ModulePath
if ($Prepend)
{
$env:PSModulePAth = $Path + [System.IO.Path]::PathSeparator + $env:PSModulePath
$env:PSModulePath = $Path + [System.IO.Path]::PathSeparator + $env:PSModulePath
}
else
{
@@ -131,6 +131,22 @@ function New-TestNestedModule
[scriptblock]::Create($newManifestCmd).Invoke()
}
function Get-DesktopModuleToUse {
$system32Path = "$env:windir\system32\WindowsPowerShell\v1.0\Modules"
$persistentMemoryModule = "PersistentMemory"
$remoteDesktopModule = "RemoteDesktop"
if (Test-Path -PathType Container "$system32Path\$persistentMemoryModule") {
return $persistentMemoryModule
} elseif (Test-Path -PathType Container "$system32Path\$remoteDesktopModule") {
return $remoteDesktopModule
} else {
return $null
}
}
$desktopModuleToUse = Get-DesktopModuleToUse
Describe "Get-Module with CompatiblePSEditions-checked paths" -Tag "CI" {
BeforeAll {
@@ -643,6 +659,37 @@ Describe "Additional tests for Import-Module with WinCompat" -Tag "Feature" {
'{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned", "WindowsPowerShellCompatibilityNoClobberModuleList": ["' + $ModuleName2 + '"]}' | Out-File -Force $ConfigPath
& $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Test-${ModuleName2}PSEdition;Test-$ModuleName2" | Should -Be @('Desktop','Core')
}
It "NoClobber WinCompat list in powershell.config is a Desktop-edition module" {
if (-not $desktopModuleToUse) {
throw 'Neither the "PersistentMemory" module nor the "RemoteDesktop" module is available. Please check and use a desktop-edition module that is under the System32 module path.'
}
## The 'Desktop' edition module 'PersistentMemory' (available on Windows Client) or 'RemoteDesktop' (available on Windows Server) should not be imported twice.
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
@"
{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned", "WindowsPowerShellCompatibilityNoClobberModuleList": ["$desktopModuleToUse"]}
"@ | Out-File -Force $ConfigPath
$env:PSModulePath = ''
## The desktop-edition module is listed in the no-clobber list, so we will first try loading a core-edition
## compatible version of the module before loading the remote one. The 'system32' module path will be skipped
## in this attempt, which is by-design.
## If we don't skip the 'system32' module path in this loading attempt, the desktop-edition module will be
## imported twice as a remote module, and then 'Remove-Module' won't close the WinCompat session.
$script = @"
Import-Module $desktopModuleToUse -UseWindowsPowerShell -WarningAction Ignore
Get-Module $desktopModuleToUse | ForEach-Object { `$_.ModuleType.ToString() }
(Get-PSSession | Measure-Object).Count
Remove-Module $desktopModuleToUse
(Get-PSSession | Measure-Object).Count
"@
$scriptBlock = [scriptblock]::Create($script)
$results = & $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c $scriptBlock
$results[0] | Should -BeExactly 'Script'
$results[1] | Should -BeExactly 1
$results[2] | Should -BeExactly 0
}
}
Context "Tests around PSModulePath in WinCompat process" {
@@ -1343,3 +1390,123 @@ Describe "Import-Module nested module behaviour with Edition checking" -Tag "Fea
}
}
}
Describe "WinCompat importing should check availablity of built-in modules" -Tag "CI" {
BeforeAll {
if (-not $IsWindows ) {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
return
}
## Copy the current PowerShell instance to a temp location
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "WinCompat"
$pwshDir = Join-Path $tempDir "pwsh"
$moduleDir = Join-Path $tempDir "Modules"
$savedModulePath = $env:PSModulePath
if (Test-Path $tempDir) {
Remove-Item $tempDir -Recurse -Force
}
Write-Host "Making a copy of the running PowerShell instance ..." -ForegroundColor Yellow
Copy-Item $PSHOME $pwshDir -Recurse -Force
Move-Item $pwshDir\Modules $moduleDir -Force
Write-Host "-- Done copying!" -ForegroundColor Yellow
}
AfterAll {
if (-not $IsWindows) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
return
}
$env:PSModulePath = $savedModulePath
Remove-Item $tempDir -Recurse -Force
}
It "Missing built-in modules will trigger error instead of loading the non-compatible ones in System32 directory. Running '<Command>'" -TestCases @(
@{
Command = 'Start-Transcript';
FullyQualifiedErrorId = "CouldNotAutoloadMatchingModule";
ExceptionMessage = "*'Start-Transcript'*'Microsoft.PowerShell.Host'*'Microsoft.PowerShell.Host'*'Core'*`$PSHOME*'Import-Module Microsoft.PowerShell.Host'*"
}
@{
Command = 'Import-Module Microsoft.PowerShell.Host';
FullyQualifiedErrorId = "System.InvalidOperationException,Microsoft.PowerShell.Commands.ImportModuleCommand"
ExceptionMessage = "*'Microsoft.PowerShell.Host'*'Core'*`$PSHOME*"
}
@{
Command = 'Import-Module CimCmdlets'
FullyQualifiedErrorId = "System.InvalidOperationException,Microsoft.PowerShell.Commands.ImportModuleCommand"
ExceptionMessage = "*'CimCmdlets'*'Core'*`$PSHOME*"
}
@{
Command = 'Import-Module Microsoft.PowerShell.Utility'
FullyQualifiedErrorId = "System.InvalidOperationException,Microsoft.PowerShell.Commands.ImportModuleCommand"
ExceptionMessage = "*'Microsoft.PowerShell.Utility'*'Core'*`$PSHOME*"
}
) {
param(
$Command,
$FullyQualifiedErrorId,
$ExceptionMessage
)
$template = @'
try {{
{0}
}} catch {{
$_.FullyQualifiedErrorId
$_.Exception.Message
}}
'@
$env:PSModulePath = ''
$script = $template -f $Command
$scriptBlock = [scriptblock]::Create($script)
$result = & "$pwshDir\pwsh.exe" -NoProfile -NonInteractive -c $scriptBlock
$result | Should -HaveCount 2
$result[0] | Should -BeExactly $FullyQualifiedErrorId
$result[1] | Should -BeLike $ExceptionMessage
}
It "Attempt to load a 'Desktop' edition module should fail because 'Export-PSSession' cannot be found" {
if (-not $desktopModuleToUse) {
throw 'Neither the "PersistentMemory" module nor the "RemoteDesktop" module is available. Please check and use a desktop-edition module that is under the System32 module path.'
}
$script = @"
try {
Import-Module $desktopModuleToUse -ErrorAction Stop
} catch {
`$_.FullyQualifiedErrorId
`$_.Exception.Message
}
"@
$env:PSModulePath = ''
$scriptBlock = [scriptblock]::Create($script)
$result = & "$pwshDir\pwsh.exe" -NoProfile -NonInteractive -c $scriptBlock
$result | Should -HaveCount 2
$result[0] | Should -BeExactly "CommandNotFoundException,Microsoft.PowerShell.Commands.ImportModuleCommand"
$result[1] | Should -BeLike "*'$desktopModuleToUse'*'Export-PSSession'*'Microsoft.PowerShell.Utility'*"
}
It "When built-in modules are available but not in `$PSHOME module path, things should work" {
$env:PSModulePath = ''
$result = & "$pwshDir\pwsh.exe" -NoProfile -NonInteractive -c @"
`$env:PSModulePath += ';$moduleDir'
Import-Module Microsoft.PowerShell.Utility -UseWindowsPowerShell -WarningAction Ignore
Get-Module Microsoft.PowerShell.Utility | ForEach-Object ModuleType
Get-Module Microsoft.PowerShell.Utility | Where-Object ModuleType -eq 'Manifest' | ForEach-Object Path
Get-Module Microsoft.PowerShell.Utility | Where-Object ModuleType -eq 'Script' | ForEach-Object { `$_.ExportedCommands.Keys }
"@
$result | Should -HaveCount 6
$result[0] | Should -BeExactly 'Manifest'
$result[1] | Should -BeExactly 'Script'
$result[2] | Should -BeExactly "$moduleDir\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1"
$result[3] | Should -BeExactly 'Convert-String'
$result[4] | Should -BeExactly 'ConvertFrom-String'
$result[5] | Should -BeExactly 'CFS'
}
}