Added logic in AssemblyLoadContext to load assembly from GAC. (#3981)

This commit is contained in:
Aditya Patwardhan
2017-06-15 13:25:59 -07:00
committed by Dongbo Wang
parent 440590ef8a
commit 26a44abcec
2 changed files with 166 additions and 8 deletions
@@ -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
/// </param>
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<string, string> _coreClrTypeCatalog;
private readonly Lazy<HashSet<string>> _availableDotNetAssemblyNames;
#if !UNIX
private string _winDir;
private string _gacPathMSIL;
private string _gacPath32;
private string _gacPath64;
#endif
/// <summary>
/// Assembly cache across the AppDomain
/// </summary>
@@ -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
/// <summary>
/// Try to get the specified assembly from cache
/// </summary>
@@ -400,6 +515,24 @@ namespace System.Management.Automation
PowerShellAssemblyLoadContext.InitializeSingleton(basePaths);
}
}
/// <summary>
/// Test hooks for PowershellAssemblyLoadContext
/// </summary>
public static class PowerShellAssemblyLoadContextTestHooks
{
internal static bool AllowGACLoading = true;
/// <summary>This member is used for internal test purposes.</summary>
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
@@ -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 <TestNameSuffix>" -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'
}
}
}