Skip to content

Commit bd91f48

Browse files
AndrewTravisEz13
authored andcommitted
Fix WinCompat module loading to treat Core edition modules higher priority (PowerShell#12269)
1 parent f9106ae commit bd91f48

File tree

4 files changed

+218
-38
lines changed

4 files changed

+218
-38
lines changed

src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs

Lines changed: 108 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,27 @@ private void ImportModule_ViaAssembly(ImportModuleOptions importModuleOptions, A
591591
}
592592
}
593593

594+
private PSModuleInfo ImportModule_LocallyViaName_WithTelemetry(ImportModuleOptions importModuleOptions, string name)
595+
{
596+
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, name);
597+
if (foundModule != null)
598+
{
599+
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
600+
601+
// report loading of the module in telemetry
602+
// avoid double reporting for WinCompat modules that go through CommandDiscovery\AutoloadSpecifiedModule
603+
if (!foundModule.IsWindowsPowerShellCompatModule)
604+
{
605+
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
606+
#if LEGACYTELEMETRY
607+
TelemetryAPI.ReportModuleLoad(foundModule);
608+
#endif
609+
}
610+
}
611+
612+
return foundModule;
613+
}
614+
594615
private PSModuleInfo ImportModule_LocallyViaName(ImportModuleOptions importModuleOptions, string name)
595616
{
596617
try
@@ -820,6 +841,24 @@ private PSModuleInfo ImportModule_LocallyViaName(ImportModuleOptions importModul
820841
return null;
821842
}
822843

844+
private PSModuleInfo ImportModule_LocallyViaFQName(ImportModuleOptions importModuleOptions, ModuleSpecification modulespec)
845+
{
846+
RequiredVersion = modulespec.RequiredVersion;
847+
MinimumVersion = modulespec.Version;
848+
MaximumVersion = modulespec.MaximumVersion;
849+
BaseGuid = modulespec.Guid;
850+
851+
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, modulespec.Name);
852+
853+
if (foundModule != null)
854+
{
855+
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
856+
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
857+
}
858+
859+
return foundModule;
860+
}
861+
823862
#endregion Local import
824863

825864
#region Remote import
@@ -1024,7 +1063,10 @@ private PSModuleInfo ImportModule_RemotelyViaPsrpSession_SinglePreimportedModule
10241063
{
10251064
powerShell.AddCommand("Export-PSSession");
10261065
powerShell.AddParameter("OutputModule", wildcardEscapedPath);
1027-
powerShell.AddParameter("AllowClobber", true);
1066+
if (!importModuleOptions.NoClobberExportPSSession)
1067+
{
1068+
powerShell.AddParameter("AllowClobber", true);
1069+
}
10281070
powerShell.AddParameter("Module", remoteModuleName); // remoteModulePath is currently unsupported by Get-Command and implicit remoting
10291071
powerShell.AddParameter("Force", true);
10301072
powerShell.AddParameter("FormatTypeName", "*");
@@ -1816,21 +1858,7 @@ protected override void ProcessRecord()
18161858
{
18171859
foreach (string name in Name)
18181860
{
1819-
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, name);
1820-
if (foundModule != null)
1821-
{
1822-
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
1823-
1824-
// report loading of the module in telemetry
1825-
// avoid double reporting for WinCompat modules that go through CommandDiscovery\AutoloadSpecifiedModule
1826-
if (!foundModule.IsWindowsPowerShellCompatModule)
1827-
{
1828-
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
1829-
#if LEGACYTELEMETRY
1830-
TelemetryAPI.ReportModuleLoad(foundModule);
1831-
#endif
1832-
}
1833-
}
1861+
ImportModule_LocallyViaName_WithTelemetry(importModuleOptions, name);
18341862
}
18351863
}
18361864
else if (this.ParameterSetName.Equals(ParameterSet_ViaPsrpSession, StringComparison.OrdinalIgnoreCase))
@@ -1845,17 +1873,7 @@ protected override void ProcessRecord()
18451873
{
18461874
foreach (var modulespec in FullyQualifiedName)
18471875
{
1848-
RequiredVersion = modulespec.RequiredVersion;
1849-
MinimumVersion = modulespec.Version;
1850-
MaximumVersion = modulespec.MaximumVersion;
1851-
BaseGuid = modulespec.Guid;
1852-
1853-
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, modulespec.Name);
1854-
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, modulespec.Name);
1855-
if (foundModule != null)
1856-
{
1857-
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
1858-
}
1876+
ImportModule_LocallyViaFQName(importModuleOptions, modulespec);
18591877
}
18601878
}
18611879
else if (this.ParameterSetName.Equals(ParameterSet_FQName_ViaPsrpSession, StringComparison.OrdinalIgnoreCase))
@@ -1884,19 +1902,10 @@ private bool IsModuleInDenyList(string[] moduleDenyList, string moduleName, Modu
18841902
{
18851903
Debug.Assert(string.IsNullOrEmpty(moduleName) ^ (moduleSpec == null), "Either moduleName or moduleSpec must be specified");
18861904

1887-
var exactModuleName = string.Empty;
1905+
// 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
1906+
string exactModuleName = ModuleIntrinsics.GetModuleName(moduleSpec == null ? moduleName : moduleSpec.Name);
18881907
bool match = false;
18891908

1890-
if (!string.IsNullOrEmpty(moduleName))
1891-
{
1892-
// 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
1893-
exactModuleName = Path.GetFileNameWithoutExtension(moduleName);
1894-
}
1895-
else if (moduleSpec != null)
1896-
{
1897-
exactModuleName = moduleSpec.Name;
1898-
}
1899-
19001909
foreach (var deniedModuleName in moduleDenyList)
19011910
{
19021911
// use case-insensitive module name comparison
@@ -1941,6 +1950,49 @@ private List<T> FilterModuleCollection<T>(IEnumerable<T> moduleCollection)
19411950
return filteredModuleCollection;
19421951
}
19431952

1953+
private void PrepareNoClobberWinCompatModuleImport(string moduleName, ModuleSpecification moduleSpec, ref ImportModuleOptions importModuleOptions)
1954+
{
1955+
Debug.Assert(string.IsNullOrEmpty(moduleName) ^ (moduleSpec == null), "Either moduleName or moduleSpec must be specified");
1956+
1957+
// 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
1958+
string coreModuleToLoad = ModuleIntrinsics.GetModuleName(moduleSpec == null ? moduleName : moduleSpec.Name);
1959+
1960+
var isModuleToLoadEngineModule = InitialSessionState.IsEngineModule(coreModuleToLoad);
1961+
string[] noClobberModuleList = PowerShellConfig.Instance.GetWindowsPowerShellCompatibilityNoClobberModuleList();
1962+
if (isModuleToLoadEngineModule || ((noClobberModuleList != null) && noClobberModuleList.Contains(coreModuleToLoad, StringComparer.OrdinalIgnoreCase)))
1963+
{
1964+
// if it is one of engine modules - first try to load it from $PSHOME\Modules
1965+
// otherwise rely on $env:PSModulePath (in which WinPS module location has to go after CorePS module location)
1966+
if (isModuleToLoadEngineModule)
1967+
{
1968+
string expectedCoreModulePath = Path.Combine(ModuleIntrinsics.GetPSHomeModulePath(), coreModuleToLoad);
1969+
if (Directory.Exists(expectedCoreModulePath))
1970+
{
1971+
coreModuleToLoad = expectedCoreModulePath;
1972+
}
1973+
}
1974+
1975+
if (moduleSpec == null)
1976+
{
1977+
ImportModule_LocallyViaName_WithTelemetry(importModuleOptions, coreModuleToLoad);
1978+
}
1979+
else
1980+
{
1981+
ModuleSpecification tmpModuleSpec = new ModuleSpecification()
1982+
{
1983+
Guid = moduleSpec.Guid,
1984+
MaximumVersion = moduleSpec.MaximumVersion,
1985+
Version = moduleSpec.Version,
1986+
RequiredVersion = moduleSpec.RequiredVersion,
1987+
Name = coreModuleToLoad
1988+
};
1989+
ImportModule_LocallyViaFQName(importModuleOptions, tmpModuleSpec);
1990+
}
1991+
1992+
importModuleOptions.NoClobberExportPSSession = true;
1993+
}
1994+
}
1995+
19441996
internal override IList<PSModuleInfo> ImportModulesUsingWinCompat(IEnumerable<string> moduleNames, IEnumerable<ModuleSpecification> moduleFullyQualifiedNames, ImportModuleOptions importModuleOptions)
19451997
{
19461998
IList<PSModuleInfo> moduleProxyList = new List<PSModuleInfo>();
@@ -1968,6 +2020,24 @@ internal override IList<PSModuleInfo> ImportModulesUsingWinCompat(IEnumerable<st
19682020
return new List<PSModuleInfo>();
19692021
}
19702022

2023+
// perform necessary preparations if module has to be imported with NoClobber mode
2024+
if (filteredModuleNames != null)
2025+
{
2026+
foreach(string moduleName in filteredModuleNames)
2027+
{
2028+
PrepareNoClobberWinCompatModuleImport(moduleName, null, ref importModuleOptions);
2029+
}
2030+
}
2031+
2032+
if (filteredModuleFullyQualifiedNames != null)
2033+
{
2034+
foreach(var moduleSpec in filteredModuleFullyQualifiedNames)
2035+
{
2036+
PrepareNoClobberWinCompatModuleImport(null, moduleSpec, ref importModuleOptions);
2037+
}
2038+
}
2039+
2040+
// perform the module import / proxy generation
19712041
moduleProxyList = ImportModule_RemotelyViaPsrpSession(importModuleOptions, filteredModuleNames, filteredModuleFullyQualifiedNames, WindowsPowerShellCompatRemotingSession, usingWinCompat: true);
19722042

19732043
foreach (PSModuleInfo moduleProxy in moduleProxyList)

src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ protected internal struct ImportModuleOptions
9999
/// This will be allowed when the manifest explicitly exports functions which will limit all visible module functions.
100100
/// </summary>
101101
internal bool AllowNestedModuleFunctionsToExport;
102+
103+
/// <summary>
104+
/// Flag that controls Export-PSSession -AllowClobber parameter in generating proxy modules from remote sessions.
105+
/// Historically -AllowClobber in these scenarios was set as True.
106+
/// </summary>
107+
internal bool NoClobberExportPSSession;
102108
}
103109

104110
/// <summary>

src/System.Management.Automation/engine/PSConfiguration.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ internal sealed class PowerShellConfig
5151
private const string ExecutionPolicyDefaultShellKey = "Microsoft.PowerShell:ExecutionPolicy";
5252
private const string DisableImplicitWinCompatKey = "DisableImplicitWinCompat";
5353
private const string WindowsPowerShellCompatibilityModuleDenyListKey = "WindowsPowerShellCompatibilityModuleDenyList";
54+
private const string WindowsPowerShellCompatibilityNoClobberModuleListKey = "WindowsPowerShellCompatibilityNoClobberModuleList";
5455

5556
// Provide a singleton
5657
internal static readonly PowerShellConfig Instance = new PowerShellConfig();
@@ -240,6 +241,18 @@ internal string[] GetWindowsPowerShellCompatibilityModuleDenyList()
240241
return settingValue;
241242
}
242243

244+
internal string[] GetWindowsPowerShellCompatibilityNoClobberModuleList()
245+
{
246+
string[] settingValue = ReadValueFromFile<string[]>(ConfigScope.CurrentUser, WindowsPowerShellCompatibilityNoClobberModuleListKey);
247+
if (settingValue == null)
248+
{
249+
// if the setting is not mentioned in configuration files, then the default WindowsPowerShellCompatibilityNoClobberModuleList value is null
250+
settingValue = ReadValueFromFile<string[]>(ConfigScope.AllUsers, WindowsPowerShellCompatibilityNoClobberModuleListKey);
251+
}
252+
253+
return settingValue;
254+
}
255+
243256
/// <summary>
244257
/// Corresponding settings of the original Group Policies.
245258
/// </summary>

test/powershell/Modules/Microsoft.PowerShell.Core/CompatiblePSEditions.Module.Tests.ps1

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,97 @@ Describe "Additional tests for Import-Module with WinCompat" -Tag "Feature" {
554554
}
555555
}
556556

557+
Context "Tests around Windows PowerShell Compatibility NoClobber module list" {
558+
BeforeAll {
559+
$pwsh = "$PSHOME/pwsh"
560+
Add-ModulePath $basePath
561+
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
562+
}
563+
564+
AfterAll {
565+
Restore-ModulePath
566+
}
567+
568+
It "NoClobber WinCompat import works for an engine module through command discovery" {
569+
570+
ConvertFrom-String -InputObject '1,2,3' -Delimiter ',' | Out-Null
571+
$modules = Get-Module -Name Microsoft.PowerShell.Utility
572+
$modules.Count | Should -Be 2
573+
$proxyModule = $modules | Where-Object {$_.ModuleType -eq 'Script'}
574+
$coreModule = $modules | Where-Object {$_.ModuleType -eq 'Manifest'}
575+
576+
$proxyModule.ExportedCommands.Keys | Should -Contain "ConvertFrom-String"
577+
$proxyModule.ExportedCommands.Keys | Should -Not -Contain "Get-Date"
578+
579+
$coreModule.ExportedCommands.Keys | Should -Contain "Get-Date"
580+
$coreModule.ExportedCommands.Keys | Should -Not -Contain "ConvertFrom-String"
581+
582+
$proxyModule | Remove-Module -Force
583+
}
584+
585+
It "NoClobber WinCompat import works for an engine module through -UseWindowsPowerShell parameter" {
586+
587+
Import-Module Microsoft.PowerShell.Management -UseWindowsPowerShell
588+
589+
$modules = Get-Module -Name Microsoft.PowerShell.Management
590+
$modules.Count | Should -Be 2
591+
$proxyModule = $modules | Where-Object {$_.ModuleType -eq 'Script'}
592+
$coreModule = $modules | Where-Object {$_.ModuleType -eq 'Manifest'}
593+
594+
$proxyModule.ExportedCommands.Keys | Should -Contain "Get-WmiObject"
595+
$proxyModule.ExportedCommands.Keys | Should -Not -Contain "Get-Item"
596+
597+
$coreModule.ExportedCommands.Keys | Should -Contain "Get-Item"
598+
$coreModule.ExportedCommands.Keys | Should -Not -Contain "Get-WmiObject"
599+
600+
$proxyModule | Remove-Module -Force
601+
}
602+
603+
It "NoClobber WinCompat import works with ModuleSpecifications" {
604+
605+
Import-Module -UseWindowsPowerShell -FullyQualifiedName @{ModuleName='Microsoft.PowerShell.Utility';ModuleVersion='0.0'}
606+
607+
$modules = Get-Module -Name Microsoft.PowerShell.Utility
608+
$modules.Count | Should -Be 2
609+
$proxyModule = $modules | Where-Object {$_.ModuleType -eq 'Script'}
610+
$coreModule = $modules | Where-Object {$_.ModuleType -eq 'Manifest'}
611+
612+
$proxyModule.ExportedCommands.Keys | Should -Contain "ConvertFrom-String"
613+
$proxyModule.ExportedCommands.Keys | Should -Not -Contain "Get-Date"
614+
615+
$coreModule.ExportedCommands.Keys | Should -Contain "Get-Date"
616+
$coreModule.ExportedCommands.Keys | Should -Not -Contain "ConvertFrom-String"
617+
618+
$proxyModule | Remove-Module -Force
619+
}
620+
621+
It "NoClobber WinCompat list in powershell.config is missing " {
622+
'{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned"}' | Out-File -Force $ConfigPath
623+
& $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'
624+
}
625+
626+
It "NoClobber WinCompat list in powershell.config is empty " {
627+
'{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned", "WindowsPowerShellCompatibilityNoClobberModuleList": []}' | Out-File -Force $ConfigPath
628+
& $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'
629+
}
630+
631+
It "NoClobber WinCompat list in powershell.config is working " {
632+
$targetModuleFolder = Join-Path $TestDrive "TempWinCompatModuleFolder"
633+
Copy-Item -Path "$basePath\$ModuleName2" -Destination "$targetModuleFolder\$ModuleName2" -Recurse -Force
634+
$env:PSModulePath = $targetModuleFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath
635+
636+
$psm1 = Get-ChildItem -Recurse -Path $targetModuleFolder -Filter "$ModuleName2.psm1"
637+
"function Test-$ModuleName2 { `$PSVersionTable.PSEdition }" | Out-File -FilePath $psm1.FullName -Force
638+
639+
# Now Core version of the module has 1 function: Test-$ModuleName2 (returns 'Core')
640+
# and WinPS version of the module has 2 functions: Test-$ModuleName2 (returns '$true'), Test-${ModuleName2}PSEdition (returns 'Desktop')
641+
# when NoClobber WinCompat import is working Test-$ModuleName2 should return 'Core'
642+
643+
'{"Microsoft.PowerShell:ExecutionPolicy": "RemoteSigned", "WindowsPowerShellCompatibilityNoClobberModuleList": ["' + $ModuleName2 + '"]}' | Out-File -Force $ConfigPath
644+
& $pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Test-${ModuleName2}PSEdition;Test-$ModuleName2" | Should -Be @('Desktop','Core')
645+
}
646+
}
647+
557648
Context "Tests around PSModulePath in WinCompat process" {
558649
BeforeAll {
559650
$pwsh = "$PSHOME/pwsh"

0 commit comments

Comments
 (0)