Fix WinCompat
module loading to treat Core edition modules higher priority (#12269)
This commit is contained in:
parent
b7c66ab3ef
commit
0c695376b7
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user