diff --git a/src/Microsoft.PowerShell.CoreCLR.AssemblyLoadContext/CoreCLR/CorePsAssemblyLoadContext.cs b/src/Microsoft.PowerShell.CoreCLR.AssemblyLoadContext/CoreCLR/CorePsAssemblyLoadContext.cs index 2250b432dd..940c4e2464 100644 --- a/src/Microsoft.PowerShell.CoreCLR.AssemblyLoadContext/CoreCLR/CorePsAssemblyLoadContext.cs +++ b/src/Microsoft.PowerShell.CoreCLR.AssemblyLoadContext/CoreCLR/CorePsAssemblyLoadContext.cs @@ -13,6 +13,8 @@ using System.Reflection; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Runtime.Loader; +using System.Text; +using System.Linq; namespace System.Management.Automation { @@ -61,6 +63,11 @@ namespace System.Management.Automation /// private PowerShellAssemblyLoadContext(string basePaths) { +#if !UNIX + // Set GAC related member variables to null + _winDir = _gacPath32 = _gacPath64 = _gacPathMSIL = null; +#endif + // FIRST: Validate and populate probing paths if (string.IsNullOrEmpty(basePaths)) { @@ -103,6 +110,13 @@ namespace System.Management.Automation private readonly Dictionary _coreClrTypeCatalog; private readonly Lazy> _availableDotNetAssemblyNames; +#if !UNIX + private string _winDir; + private string _gacPathMSIL; + private string _gacPath32; + private string _gacPath64; +#endif + /// /// Assembly cache across the AppDomain /// @@ -251,7 +265,15 @@ namespace System.Management.Automation // In this case, return null so that other Resolving event handlers can kick in to resolve the request. if (!isAssemblyFileFound || !isAssemblyFileMatching) { +#if !UNIX + //Try loading from GAC + if(!TryFindInGAC(assemblyName, out asmFilePath)) + { + return null; + } +#else return null; +#endif } asmLoaded = asmFilePath.EndsWith(".ni.dll", StringComparison.OrdinalIgnoreCase) @@ -267,6 +289,99 @@ namespace System.Management.Automation return asmLoaded; } +#if !UNIX + // Try to find the assembly in GAC by looking up the directories in well know locations. + // First try to find in GAC_MSIL, then depending on process bitness; GAC_64 or GAC32. + // If there are multiple version of the assembly, load the latest. + private bool TryFindInGAC(AssemblyName assemblyName, out string assemblyFilePath) + { + bool assemblyFound = false; + assemblyFilePath = null; + char dirSeparator = IO.Path.DirectorySeparatorChar; + + if (PowerShellAssemblyLoadContextTestHooks.AllowGACLoading) + { + if(String.IsNullOrEmpty(_winDir)) + { + //cache value of '_winDir' folder in member variable. + _winDir = Environment.GetEnvironmentVariable("winDir"); + } + + if (String.IsNullOrEmpty(_gacPathMSIL)) + { + //cache value of '_gacPathMSIL' folder in member variable. + _gacPathMSIL = $"{_winDir}{dirSeparator}Microsoft.NET{dirSeparator}assembly{dirSeparator}GAC_MSIL"; + } + + assemblyFound = FindInGac(_gacPathMSIL, assemblyName, out assemblyFilePath); + + if(!assemblyFound) + { + string gacBitnessAwarePath = null; + + if(Environment.Is64BitProcess) + { + if(String.IsNullOrEmpty(_gacPath64)) + { + //cache value of '_gacPath64' folder in member variable. + _gacPath64 = $"{_winDir}{dirSeparator}Microsoft.NET{dirSeparator}assembly{dirSeparator}GAC_64"; + } + + gacBitnessAwarePath = _gacPath64; + } + else + { + if(String.IsNullOrEmpty(_gacPath32)) + { + //cache value of '_gacPath32' folder in member variable. + _gacPath32 = $"{_winDir}{dirSeparator}Microsoft.NET{dirSeparator}assembly{dirSeparator}GAC_32"; + } + + gacBitnessAwarePath = _gacPath32; + } + + assemblyFound = FindInGac(gacBitnessAwarePath, assemblyName, out assemblyFilePath); + } + } + + return assemblyFound; + } + + // Find the assembly under 'gacRoot' and select the latest version. + private bool FindInGac(string gacRoot, AssemblyName assemblyName, out string assemblyPath) + { + bool assemblyFound = false; + assemblyPath = null; + + char dirSeparator = IO.Path.DirectorySeparatorChar; + string tempAssemblyDirPath = $"{gacRoot}{dirSeparator}{assemblyName.Name}"; + + if(Directory.Exists(tempAssemblyDirPath)) + { + //Enumerate all directories, sort by name and select the last. This selects the latest version. + var chosenVersionDirectory = Directory.GetDirectories(tempAssemblyDirPath).OrderBy(d => d).LastOrDefault(); + + if(!String.IsNullOrEmpty(chosenVersionDirectory)) + { + //Select first or default as the directory will contain only one assembly. If nothing then default is null; + var foundAssemblyPath = Directory.GetFiles(chosenVersionDirectory, $"{assemblyName.Name}*").FirstOrDefault(); + + if(!String.IsNullOrEmpty(foundAssemblyPath)) + { + AssemblyName asmNameFound = AssemblyLoadContext.GetAssemblyName(foundAssemblyPath); + if (IsAssemblyMatching(assemblyName, asmNameFound)) + { + assemblyPath = foundAssemblyPath; + assemblyFound = true; + } + } + } + } + + return assemblyFound; + } +#endif + /// /// Try to get the specified assembly from cache /// @@ -400,6 +515,24 @@ namespace System.Management.Automation PowerShellAssemblyLoadContext.InitializeSingleton(basePaths); } } + + /// + /// Test hooks for PowershellAssemblyLoadContext + /// + public static class PowerShellAssemblyLoadContextTestHooks + { + internal static bool AllowGACLoading = true; + + /// This member is used for internal test purposes. + public static void SetTestHook(string property, bool value) + { + var fieldInfo = typeof(PowerShellAssemblyLoadContextTestHooks).GetField(property, BindingFlags.Static | BindingFlags.NonPublic); + if (fieldInfo != null) + { + fieldInfo.SetValue(null, value); + } + } + } } #endif diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 index 2da49f4874..f56e52a93c 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 @@ -30,9 +30,9 @@ Describe "Import-Module with ScriptsToProcess" -Tags "CI" { New-Item $moduleRootPath -ItemType Directory -Force | Out-Null Push-Location $moduleRootPath - "1 | Out-File out.txt -Append -NoNewline" | Out-File script1.ps1 - "2 | Out-File out.txt -Append -NoNewline" | Out-File script2.ps1 - New-ModuleManifest module1.psd1 -ScriptsToProcess script1.ps1 + "1 | Out-File out.txt -Append -NoNewline" | Out-File script1.ps1 + "2 | Out-File out.txt -Append -NoNewline" | Out-File script2.ps1 + New-ModuleManifest module1.psd1 -ScriptsToProcess script1.ps1 New-ModuleManifest module2.psd1 -ScriptsToProcess script2.ps1 -NestedModules module1.psd1 } @@ -50,11 +50,11 @@ Describe "Import-Module with ScriptsToProcess" -Tags "CI" { Remove-Item out.txt -Force -ErrorAction SilentlyContinue } - $testCases = @( - @{ TestNameSuffix = 'for top-level module'; ipmoParms = @{'Name'='.\module1.psd1'}; Expected = '1' } - @{ TestNameSuffix = 'for top-level and nested module'; ipmoParms = @{'Name'='.\module2.psd1'}; Expected = '21' } - @{ TestNameSuffix = 'for top-level module when -Version is specified'; ipmoParms = @{'Name'='.\module1.psd1'; 'Version'='1.0'}; Expected = '1' } - @{ TestNameSuffix = 'for top-level and nested module when -Version is specified'; ipmoParms = @{'Name'='.\module2.psd1'; 'Version'='1.0'}; Expected = '21' } + $testCases = @( + @{ TestNameSuffix = 'for top-level module'; ipmoParms = @{'Name'='.\module1.psd1'}; Expected = '1' } + @{ TestNameSuffix = 'for top-level and nested module'; ipmoParms = @{'Name'='.\module2.psd1'}; Expected = '21' } + @{ TestNameSuffix = 'for top-level module when -Version is specified'; ipmoParms = @{'Name'='.\module1.psd1'; 'Version'='1.0'}; Expected = '1' } + @{ TestNameSuffix = 'for top-level and nested module when -Version is specified'; ipmoParms = @{'Name'='.\module2.psd1'; 'Version'='1.0'}; Expected = '21' } ) It "Verify ScriptsToProcess are executed " -TestCases $testCases { @@ -63,3 +63,28 @@ Describe "Import-Module with ScriptsToProcess" -Tags "CI" { Get-Content out.txt | Should Be $Expected } } + +Describe "Import-Module for Binary Modules in GAC" -Tags 'CI' { + Context "Modules are not loaded from GAC" { + BeforeAll { + [System.Management.Automation.PowerShellAssemblyLoadContextTestHooks]::SetTestHook('AllowGACLoading', $false) + } + + AfterAll { + [System.Management.Automation.PowerShellAssemblyLoadContextTestHooks]::SetTestHook('AllowGACLoading', $true) + } + + It "Load PSScheduledJob from Windows Powershell Modules folder should fail" -Skip:(-not $IsWindows) { + $modulePath = Join-Path $env:windir "System32/WindowsPowershell/v1.0/Modules/PSScheduledJob" + { Import-Module $modulePath -ErrorAction SilentlyContinue } | ShouldBeErrorId 'FormatXmlUpdateException,Microsoft.PowerShell.Commands.ImportModuleCommand' + } + } + + Context "Modules are loaded from GAC" { + It "Load PSScheduledJob from Windows Powershell Modules folder" -Skip:(-not $IsWindows) { + $modulePath = Join-Path $env:windir "System32/WindowsPowershell/v1.0/Modules/PSScheduledJob" + Import-Module $modulePath + (Get-Command New-JobTrigger).Name | Should Be 'New-JobTrigger' + } + } +}