Fix WinCompat module loading to treat Core edition modules higher priority (#12269)

This commit is contained in:
Andrew 2020-04-16 16:57:55 -07:00 committed by GitHub
parent b7c66ab3ef
commit 0c695376b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 218 additions and 38 deletions

View File

@ -591,6 +591,27 @@ namespace Microsoft.PowerShell.Commands
}
}
private PSModuleInfo ImportModule_LocallyViaName_WithTelemetry(ImportModuleOptions importModuleOptions, string name)
{
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, name);
if (foundModule != null)
{
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
// report loading of the module in telemetry
// avoid double reporting for WinCompat modules that go through CommandDiscovery\AutoloadSpecifiedModule
if (!foundModule.IsWindowsPowerShellCompatModule)
{
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
#if LEGACYTELEMETRY
TelemetryAPI.ReportModuleLoad(foundModule);
#endif
}
}
return foundModule;
}
private PSModuleInfo ImportModule_LocallyViaName(ImportModuleOptions importModuleOptions, string name)
{
try
@ -820,6 +841,24 @@ namespace Microsoft.PowerShell.Commands
return null;
}
private PSModuleInfo ImportModule_LocallyViaFQName(ImportModuleOptions importModuleOptions, ModuleSpecification modulespec)
{
RequiredVersion = modulespec.RequiredVersion;
MinimumVersion = modulespec.Version;
MaximumVersion = modulespec.MaximumVersion;
BaseGuid = modulespec.Guid;
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, modulespec.Name);
if (foundModule != null)
{
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
}
return foundModule;
}
#endregion Local import
#region Remote import
@ -1024,7 +1063,10 @@ namespace Microsoft.PowerShell.Commands
{
powerShell.AddCommand("Export-PSSession");
powerShell.AddParameter("OutputModule", wildcardEscapedPath);
powerShell.AddParameter("AllowClobber", true);
if (!importModuleOptions.NoClobberExportPSSession)
{
powerShell.AddParameter("AllowClobber", true);
}
powerShell.AddParameter("Module", remoteModuleName); // remoteModulePath is currently unsupported by Get-Command and implicit remoting
powerShell.AddParameter("Force", true);
powerShell.AddParameter("FormatTypeName", "*");
@ -1816,21 +1858,7 @@ namespace Microsoft.PowerShell.Commands
{
foreach (string name in Name)
{
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, name);
if (foundModule != null)
{
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
// report loading of the module in telemetry
// avoid double reporting for WinCompat modules that go through CommandDiscovery\AutoloadSpecifiedModule
if (!foundModule.IsWindowsPowerShellCompatModule)
{
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
#if LEGACYTELEMETRY
TelemetryAPI.ReportModuleLoad(foundModule);
#endif
}
}
ImportModule_LocallyViaName_WithTelemetry(importModuleOptions, name);
}
}
else if (this.ParameterSetName.Equals(ParameterSet_ViaPsrpSession, StringComparison.OrdinalIgnoreCase))
@ -1845,17 +1873,7 @@ namespace Microsoft.PowerShell.Commands
{
foreach (var modulespec in FullyQualifiedName)
{
RequiredVersion = modulespec.RequiredVersion;
MinimumVersion = modulespec.Version;
MaximumVersion = modulespec.MaximumVersion;
BaseGuid = modulespec.Guid;
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, modulespec.Name);
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, modulespec.Name);
if (foundModule != null)
{
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
}
ImportModule_LocallyViaFQName(importModuleOptions, modulespec);
}
}
else if (this.ParameterSetName.Equals(ParameterSet_FQName_ViaPsrpSession, StringComparison.OrdinalIgnoreCase))
@ -1884,19 +1902,10 @@ namespace Microsoft.PowerShell.Commands
{
Debug.Assert(string.IsNullOrEmpty(moduleName) ^ (moduleSpec == null), "Either moduleName or moduleSpec must be specified");
var exactModuleName = string.Empty;
// 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 exactModuleName = ModuleIntrinsics.GetModuleName(moduleSpec == null ? moduleName : moduleSpec.Name);
bool match = false;
if (!string.IsNullOrEmpty(moduleName))
{
// 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
exactModuleName = Path.GetFileNameWithoutExtension(moduleName);
}
else if (moduleSpec != null)
{
exactModuleName = moduleSpec.Name;
}
foreach (var deniedModuleName in moduleDenyList)
{
// use case-insensitive module name comparison
@ -1941,6 +1950,49 @@ namespace Microsoft.PowerShell.Commands
return filteredModuleCollection;
}
private void PrepareNoClobberWinCompatModuleImport(string moduleName, ModuleSpecification moduleSpec, ref ImportModuleOptions importModuleOptions)
{
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);
var isModuleToLoadEngineModule = InitialSessionState.IsEngineModule(coreModuleToLoad);
string[] noClobberModuleList = PowerShellConfig.Instance.GetWindowsPowerShellCompatibilityNoClobberModuleList();
if (isModuleToLoadEngineModule || ((noClobberModuleList != null) && noClobberModuleList.Contains(coreModuleToLoad, StringComparer.OrdinalIgnoreCase)))
{
// 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)
{
string expectedCoreModulePath = Path.Combine(ModuleIntrinsics.GetPSHomeModulePath(), coreModuleToLoad);
if (Directory.Exists(expectedCoreModulePath))
{
coreModuleToLoad = expectedCoreModulePath;
}
}
if (moduleSpec == null)
{
ImportModule_LocallyViaName_WithTelemetry(importModuleOptions, coreModuleToLoad);
}
else
{
ModuleSpecification tmpModuleSpec = new ModuleSpecification()
{
Guid = moduleSpec.Guid,
MaximumVersion = moduleSpec.MaximumVersion,
Version = moduleSpec.Version,
RequiredVersion = moduleSpec.RequiredVersion,
Name = coreModuleToLoad
};
ImportModule_LocallyViaFQName(importModuleOptions, tmpModuleSpec);
}
importModuleOptions.NoClobberExportPSSession = true;
}
}
internal override IList<PSModuleInfo> ImportModulesUsingWinCompat(IEnumerable<string> moduleNames, IEnumerable<ModuleSpecification> moduleFullyQualifiedNames, ImportModuleOptions importModuleOptions)
{
IList<PSModuleInfo> moduleProxyList = new List<PSModuleInfo>();
@ -1968,6 +2020,24 @@ namespace Microsoft.PowerShell.Commands
return new List<PSModuleInfo>();
}
// perform necessary preparations if module has to be imported with NoClobber mode
if (filteredModuleNames != null)
{
foreach(string moduleName in filteredModuleNames)
{
PrepareNoClobberWinCompatModuleImport(moduleName, null, ref importModuleOptions);
}
}
if (filteredModuleFullyQualifiedNames != null)
{
foreach(var moduleSpec in filteredModuleFullyQualifiedNames)
{
PrepareNoClobberWinCompatModuleImport(null, moduleSpec, ref importModuleOptions);
}
}
// perform the module import / proxy generation
moduleProxyList = ImportModule_RemotelyViaPsrpSession(importModuleOptions, filteredModuleNames, filteredModuleFullyQualifiedNames, WindowsPowerShellCompatRemotingSession, usingWinCompat: true);
foreach (PSModuleInfo moduleProxy in moduleProxyList)

View File

@ -99,6 +99,12 @@ namespace Microsoft.PowerShell.Commands
/// This will be allowed when the manifest explicitly exports functions which will limit all visible module functions.
/// </summary>
internal bool AllowNestedModuleFunctionsToExport;
/// <summary>
/// Flag that controls Export-PSSession -AllowClobber parameter in generating proxy modules from remote sessions.
/// Historically -AllowClobber in these scenarios was set as True.
/// </summary>
internal bool NoClobberExportPSSession;
}
/// <summary>

View File

@ -51,6 +51,7 @@ namespace System.Management.Automation.Configuration
private const string ExecutionPolicyDefaultShellKey = "Microsoft.PowerShell:ExecutionPolicy";
private const string DisableImplicitWinCompatKey = "DisableImplicitWinCompat";
private const string WindowsPowerShellCompatibilityModuleDenyListKey = "WindowsPowerShellCompatibilityModuleDenyList";
private const string WindowsPowerShellCompatibilityNoClobberModuleListKey = "WindowsPowerShellCompatibilityNoClobberModuleList";
// Provide a singleton
internal static readonly PowerShellConfig Instance = new PowerShellConfig();
@ -240,6 +241,18 @@ namespace System.Management.Automation.Configuration
return settingValue;
}
internal string[] GetWindowsPowerShellCompatibilityNoClobberModuleList()
{
string[] settingValue = ReadValueFromFile<string[]>(ConfigScope.CurrentUser, WindowsPowerShellCompatibilityNoClobberModuleListKey);
if (settingValue == null)
{
// if the setting is not mentioned in configuration files, then the default WindowsPowerShellCompatibilityNoClobberModuleList value is null
settingValue = ReadValueFromFile<string[]>(ConfigScope.AllUsers, WindowsPowerShellCompatibilityNoClobberModuleListKey);
}
return settingValue;
}
/// <summary>
/// Corresponding settings of the original Group Policies.
/// </summary>

View File

@ -554,6 +554,97 @@ Describe "Additional tests for Import-Module with WinCompat" -Tag "Feature" {
}
}
Context "Tests around Windows PowerShell Compatibility NoClobber module list" {
BeforeAll {
$pwsh = "$PSHOME/pwsh"
Add-ModulePath $basePath
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
}
AfterAll {
Restore-ModulePath
}
It "NoClobber WinCompat import works for an engine module through command discovery" {
ConvertFrom-String -InputObject '1,2,3' -Delimiter ',' | Out-Null
$modules = Get-Module -Name Microsoft.PowerShell.Utility
$modules.Count | Should -Be 2
$proxyModule = $modules | Where-Object {$_.ModuleType -eq 'Script'}
$coreModule = $modules | Where-Object {$_.ModuleType -eq 'Manifest'}
$proxyModule.ExportedCommands.Keys | Should -Contain "ConvertFrom-String"
$proxyModule.ExportedCommands.Keys | Should -Not -Contain "Get-Date"
$coreModule.ExportedCommands.Keys | Should -Contain "Get-Date"
$coreModule.ExportedCommands.Keys | Should -Not -Contain "ConvertFrom-String"
$proxyModule | Remove-Module -Force
}
It "NoClobber WinCompat import works for an engine module through -UseWindowsPowerShell parameter" {
Import-Module Microsoft.PowerShell.Management -UseWindowsPowerShell
$modules = Get-Module -Name Microsoft.PowerShell.Management
$modules.Count | Should -Be 2
$proxyModule = $modules | Where-Object {$_.ModuleType -eq 'Script'}
$coreModule = $modules | Where-Object {$_.ModuleType -eq 'Manifest'}
$proxyModule.ExportedCommands.Keys | Should -Contain "Get-WmiObject"
$proxyModule.ExportedCommands.Keys | Should -Not -Contain "Get-Item"
$coreModule.ExportedCommands.Keys | Should -Contain "Get-Item"
$coreModule.ExportedCommands.Keys | Should -Not -Contain "Get-WmiObject"
$proxyModule | Remove-Module -Force
}
It "NoClobber WinCompat import works with ModuleSpecifications" {
Import-Module -UseWindowsPowerShell -FullyQualifiedName @{ModuleName='Microsoft.PowerShell.Utility';ModuleVersion='0.0'}
$modules = Get-Module -Name Microsoft.PowerShell.Utility
$modules.Count | Should -Be 2
$proxyModule = $modules | Where-Object {$_.ModuleType -eq 'Script'}
$coreModule = $modules | Where-Object {$_.ModuleType -eq 'Manifest'}
$proxyModule.ExportedCommands.Keys | Should -Contain "ConvertFrom-String"
$proxyModule.ExportedCommands.Keys | Should -Not -Contain "Get-Date"
$coreModule.ExportedCommands.Keys | Should -Contain "Get-Date"
$coreModule.ExportedCommands.Keys | Should -Not -Contain "ConvertFrom-String"
$proxyModule | Remove-Module -Force
}
It "NoClobber WinCompat list in powershell.config is missing " {
'{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned"}' | Out-File -Force $ConfigPath
& $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2 -WarningAction Ignore;Test-${ModuleName2}PSEdition" | Should -Be 'Desktop'
}
It "NoClobber WinCompat list in powershell.config is empty " {
'{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned", "WindowsPowerShellCompatibilityNoClobberModuleList": []}' | Out-File -Force $ConfigPath
& $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2 -WarningAction Ignore;Test-${ModuleName2}PSEdition" | Should -Be 'Desktop'
}
It "NoClobber WinCompat list in powershell.config is working " {
$targetModuleFolder = Join-Path $TestDrive "TempWinCompatModuleFolder"
Copy-Item -Path "$basePath\$ModuleName2" -Destination "$targetModuleFolder\$ModuleName2" -Recurse -Force
$env:PSModulePath = $targetModuleFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath
$psm1 = Get-ChildItem -Recurse -Path $targetModuleFolder -Filter "$ModuleName2.psm1"
"function Test-$ModuleName2 { `$PSVersionTable.PSEdition }" | Out-File -FilePath $psm1.FullName -Force
# Now Core version of the module has 1 function: Test-$ModuleName2 (returns 'Core')
# and WinPS version of the module has 2 functions: Test-$ModuleName2 (returns '$true'), Test-${ModuleName2}PSEdition (returns 'Desktop')
# when NoClobber WinCompat import is working Test-$ModuleName2 should return 'Core'
'{"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')
}
}
Context "Tests around PSModulePath in WinCompat process" {
BeforeAll {
$pwsh = "$PSHOME/pwsh"