Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from Jan 12, 2022
25 changes: 14 additions & 11 deletions src/System.Management.Automation/engine/CommandDiscovery.cs
Expand Up @@ -1091,24 +1091,27 @@ private static CommandInfo InvokeCommandNotFoundHandler(string commandName, Exec

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 == null || matchingModule.Count == 0)
daxian-dbw marked this conversation as resolved.
Show resolved Hide resolved
{
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;
nameof(DiscoveryExceptions.CouldNotAutoImportMatchingModule),
errorMessage);
}

result = LookupCommandInfo(commandName, commandTypes, searchResolutionOptions, commandOrigin, context);
Expand Down
55 changes: 11 additions & 44 deletions src/System.Management.Automation/engine/InitialSessionState.cs
Expand Up @@ -1663,11 +1663,6 @@ public InitialSessionState Clone()

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);
Expand Down Expand Up @@ -3801,34 +3796,26 @@ public PSSnapInInfo ImportPSSnapIn(string name, out PSSnapInException warning)

// 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 _);
daxian-dbw marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down Expand Up @@ -3861,12 +3848,6 @@ internal PSSnapInInfo ImportPSSnapIn(PSSnapInInfo psSnapInInfo, out PSSnapInExce
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;

Expand Down Expand Up @@ -3985,29 +3966,16 @@ internal PSSnapInInfo ImportPSSnapIn(PSSnapInInfo psSnapInInfo, out PSSnapInExce
}
}

ImportedSnapins.Add(psSnapInInfo.Name, psSnapInInfo);
return psSnapInInfo;
}

internal List<PSSnapInInfo> GetPSSnapIn(string psSnapinName)
{
List<PSSnapInInfo> loadedSnapins = null;
foreach (var defaultSnapin in defaultSnapins)
{
if (defaultSnapin.Name.Equals(psSnapinName, StringComparison.OrdinalIgnoreCase))
{
if (loadedSnapins == null)
{
loadedSnapins = new List<PSSnapInInfo>();
}

loadedSnapins.Add(defaultSnapin);
}
}

PSSnapInInfo importedSnapin = null;
if (ImportedSnapins.TryGetValue(psSnapinName, out importedSnapin))
if (ImportedSnapins.TryGetValue(psSnapinName, out PSSnapInInfo importedSnapin))
{
if (loadedSnapins == null)
if (loadedSnapins is null)
{
loadedSnapins = new List<PSSnapInInfo>();
}
Expand Down Expand Up @@ -4886,7 +4854,6 @@ internal static void RemoveAllDrivesForProvider(ProviderInfo pi, SessionStateInt

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)
Expand Down
Expand Up @@ -762,7 +762,7 @@ private PSModuleInfo ImportModule_LocallyViaName(ImportModuleOptions importModul
// 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 = GetEngineSnapIn(Context, name);

// Return the command if we found a module
if (snapin != null)
Expand Down
73 changes: 50 additions & 23 deletions src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs
Expand Up @@ -280,6 +280,18 @@ internal List<WildcardPattern> MatchAll
"ModuleVersion"
};

private static readonly HashSet<string> s_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.
Expand Down Expand Up @@ -2375,17 +2387,15 @@ private IEnumerable<PSModuleInfo> CreateFakeModuleObject(IEnumerable<ModuleSpeci
{
if (importingModule)
{
// In case the module to be loaded in WinCompat mode is a PowerShell built-in module, such as the Utility module,
// we need to check whether the built-in module is actually available in the $PSHOME module path.
// This is because a user may not correctly deploy the built-in modules, and that would result in some confusing
// errors when module auto-loading silently attempts to load those modules from the 'System32' module path.
CheckAvailabilityOfBuiltInModules(ModuleIntrinsics.GetModuleName(moduleManifestPath));
IList<PSModuleInfo> moduleProxies = ImportModulesUsingWinCompat(new string[] { moduleManifestPath }, null, 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
Expand Down Expand Up @@ -3706,7 +3716,7 @@ internal static object IsModuleLoaded(ExecutionContext context, ModuleSpecificat
// 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 = GetEngineSnapIn(context, requiredModule.Name);
if (result != null)
{
loaded = true;
Expand Down Expand Up @@ -4883,10 +4893,37 @@ internal static void SyncCurrentLocationHandler(object sender, LocationChangedEv
}
}

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(); }

private void CheckAvailabilityOfBuiltInModules(string moduleName)
{
// Check if the module is a PowerShell built-in module. If so, normalize the module name; if not, skip the module.
if (!s_builtInModules.TryGetValue(moduleName, out moduleName))
{
return;
}

// When it was imported as a snapin, we skip the check because it's OK to not have the module available in such case.
if (GetEngineSnapIn(Context, moduleName) is not null)
{
return;
}

// Check if the module exists in the $PSHOME module path. If not, throws exception.
string psHomeModulePath = ModuleIntrinsics.GetPSHomeModulePath();
string moduleFolder = Path.Join(psHomeModulePath, moduleName);
if (!Directory.Exists(moduleFolder))
{
throw new InvalidOperationException(
StringUtil.Format(
Modules.CannotFindBuiltInModules,
moduleName,
psHomeModulePath));
}
}

private void RemoveTypesAndFormatting(
IList<string> formatFilesToRemove,
IList<string> typeFilesToRemove)
Expand Down Expand Up @@ -7364,19 +7401,9 @@ private static bool HasInvalidCharacters(string commandName)
/// </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 (context.CurrentRunspace.InitialSessionState.ImportedSnapins.TryGetValue(name, out PSSnapInInfo snapin))
{
if (string.Equals(snapin.Name, name, StringComparison.OrdinalIgnoreCase))
return snapin;
return snapin;
}

return null;
Expand Down
Expand Up @@ -976,20 +976,10 @@ internal static string GetPSHomeModulePath()
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\\");
psHome = psHome.ToLowerInvariant().Replace(@"\syswow64\", @"\system32\");
daxian-dbw marked this conversation as resolved.
Show resolved Hide resolved
#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)
{
Expand Down
Expand Up @@ -121,7 +121,7 @@ internal static IEnumerable<string> GetAllAvailableModuleFiles(string topDirecto
#if UNIX
return true;
#else
if (!ModuleUtils.IsOnSystem32ModulePath(moduleManifestPath))
if (!IsOnSystem32ModulePath(moduleManifestPath))
{
return true;
}
Expand Down
Expand Up @@ -221,11 +221,7 @@ public override InitialSessionState InitialSessionState
{
get
{
#pragma warning disable 56503

throw PSTraceSource.NewNotImplementedException();

#pragma warning restore 56503
}
}

Expand All @@ -236,11 +232,7 @@ public override JobManager JobManager
{
get
{
#pragma warning disable 56503

throw PSTraceSource.NewNotImplementedException();

#pragma warning restore 56503
}
}

Expand Down
Expand Up @@ -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>
daxian-dbw marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="CouldNotAutoImportModule" xml:space="preserve">
<value>The module '{0}' could not be loaded. For more information, run 'Import-Module {0}'.</value>
</data>
Expand Down
3 changes: 3 additions & 0 deletions src/System.Management.Automation/resources/Modules.resx
Expand Up @@ -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="CannotFindBuiltInModules" xml:space="preserve">
<value>The built-in module '{0}' cannot be found under the $PSHOME module path '{1}'. Please make sure the PowerShell built-in modules are available because they are required for PowerShell to function properly.</value>
</data>
</root>