From fa98fde6357b3c3fccf6474c1d7bb0b9ecd0dc69 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 18 Jul 2023 11:12:56 +0300 Subject: [PATCH 1/2] Fix #3207 - issue with colliding assemblies --- .../Base/DependencyAssemblyLoadContext.cs | 114 ++++++------- .../Base/PnPPowerShellModuleInitializer.cs | 157 +++++++++++++++++- 2 files changed, 205 insertions(+), 66 deletions(-) diff --git a/src/Commands/Base/DependencyAssemblyLoadContext.cs b/src/Commands/Base/DependencyAssemblyLoadContext.cs index 1442cd2a6..a2ea84851 100644 --- a/src/Commands/Base/DependencyAssemblyLoadContext.cs +++ b/src/Commands/Base/DependencyAssemblyLoadContext.cs @@ -1,57 +1,57 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Management.Automation; -using System.Reflection; -using System.Runtime.Loader; -using System.Text; - -namespace PnP.PowerShell.Commands -{ - public class DependencyAssemblyLoadContext : AssemblyLoadContext - { - private static readonly string s_psHome = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - - private static readonly ConcurrentDictionary s_dependencyLoadContexts = new ConcurrentDictionary(); - - internal static DependencyAssemblyLoadContext GetForDirectory(string directoryPath) - { - return s_dependencyLoadContexts.GetOrAdd(directoryPath, (path) => new DependencyAssemblyLoadContext(path)); - } - - private readonly string _dependencyDirPath; - - public DependencyAssemblyLoadContext(string dependencyDirPath) - : base(nameof(DependencyAssemblyLoadContext)) - { - _dependencyDirPath = dependencyDirPath; - } - - protected override Assembly Load(AssemblyName assemblyName) - { - string assemblyFileName = $"{assemblyName.Name}.dll"; - - // Make sure we allow other common PowerShell dependencies to be loaded by PowerShell - // But specifically exclude Microsoft.ApplicationInsightssince we want to use a different version here - if (!assemblyName.Name.Equals("Microsoft.ApplicationInsights", StringComparison.OrdinalIgnoreCase)) - { - string psHomeAsmPath = Path.Join(s_psHome, assemblyFileName); - if (File.Exists(psHomeAsmPath)) - { - // With this API, returning null means nothing is loaded - return null; - } - } - - // Now try to load the assembly from the dependency directory - string dependencyAsmPath = Path.Join(_dependencyDirPath, assemblyFileName); - if (File.Exists(dependencyAsmPath)) - { - return LoadFromAssemblyPath(dependencyAsmPath); - } - - return null; - } - } -} +//using System; +//using System.Collections.Concurrent; +//using System.Collections.Generic; +//using System.IO; +//using System.Management.Automation; +//using System.Reflection; +//using System.Runtime.Loader; +//using System.Text; + +//namespace PnP.PowerShell.Commands +//{ +// public class DependencyAssemblyLoadContext : AssemblyLoadContext +// { +// private static readonly string s_psHome = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + +// private static readonly ConcurrentDictionary s_dependencyLoadContexts = new ConcurrentDictionary(); + +// internal static DependencyAssemblyLoadContext GetForDirectory(string directoryPath) +// { +// return s_dependencyLoadContexts.GetOrAdd(directoryPath, (path) => new DependencyAssemblyLoadContext(path)); +// } + +// private readonly string _dependencyDirPath; + +// public DependencyAssemblyLoadContext(string dependencyDirPath) +// : base(nameof(DependencyAssemblyLoadContext)) +// { +// _dependencyDirPath = dependencyDirPath; +// } + +// protected override Assembly Load(AssemblyName assemblyName) +// { +// string assemblyFileName = $"{assemblyName.Name}.dll"; + +// // Make sure we allow other common PowerShell dependencies to be loaded by PowerShell +// // But specifically exclude Microsoft.ApplicationInsightssince we want to use a different version here +// if (!assemblyName.Name.Equals("Microsoft.ApplicationInsights", StringComparison.OrdinalIgnoreCase)) +// { +// string psHomeAsmPath = Path.Join(s_psHome, assemblyFileName); +// if (File.Exists(psHomeAsmPath)) +// { +// // With this API, returning null means nothing is loaded +// return null; +// } +// } + +// // Now try to load the assembly from the dependency directory +// string dependencyAsmPath = Path.Join(_dependencyDirPath, assemblyFileName); +// if (File.Exists(dependencyAsmPath)) +// { +// return LoadFromAssemblyPath(dependencyAsmPath); +// } + +// return null; +// } +// } +//} diff --git a/src/Commands/Base/PnPPowerShellModuleInitializer.cs b/src/Commands/Base/PnPPowerShellModuleInitializer.cs index 4926b1a86..f87b2c1e6 100644 --- a/src/Commands/Base/PnPPowerShellModuleInitializer.cs +++ b/src/Commands/Base/PnPPowerShellModuleInitializer.cs @@ -1,18 +1,65 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Management.Automation; using System.Reflection; using System.Runtime.Loader; -using System.Text; namespace PnP.PowerShell.Commands.Base { public class PnPPowerShellModuleInitializer : IModuleAssemblyInitializer { - private static string s_binBasePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..")); + //private static string s_binBasePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..")); + //private static string s_binCommonPath = Path.Combine(s_binBasePath, "Common"); - private static string s_binCommonPath = Path.Combine(s_binBasePath, "Common"); + private static readonly string s_binBasePath; + private static readonly string s_binCommonPath; + private static readonly HashSet s_dependencies; + private static readonly HashSet s_psEditionDependencies; + private static readonly AssemblyLoadContextProxy s_proxy; + + static PnPPowerShellModuleInitializer() + { +#if DEBUG + s_binBasePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))); + s_binCommonPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..", "..", "..", "..", "..", "src", "ALC", "bin", "Debug", "net6.0")); +#else + s_binBasePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..")); + s_binCommonPath = Path.Combine(s_binBasePath, "Common"); +#endif + s_dependencies = new HashSet(StringComparer.Ordinal); + s_psEditionDependencies = new HashSet(StringComparer.Ordinal); + s_proxy = AssemblyLoadContextProxy.CreateLoadContext("pnp-powershell-load-context"); + + // Add shared dependencies. + foreach (string filePath in Directory.EnumerateFiles(s_binBasePath, "*.dll")) + { + try + { + s_dependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); + } + catch (BadImageFormatException) + { + // Skip files without metadata. + continue; + } + } + + // Add the dependencies for the current PowerShell edition. Can be either Desktop (PS 5.1) or Core (PS 7+). + foreach (string filePath in Directory.EnumerateFiles(s_binCommonPath, "*.dll")) + { + try + { + s_psEditionDependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); + } + catch (BadImageFormatException) + { + // Skip files without metadata. + continue; + } + } + } public void OnImport() { @@ -24,15 +71,107 @@ private static Assembly ResolveAssembly_NetCore( AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName) { - // In .NET Core, PowerShell deals with assembly probing so our logic is much simpler - // We only care about our Engine assembly - if (!assemblyName.Name.Equals("PnP.PowerShell.ALC")) + if (IsAssemblyMatching(assemblyName)) + { + string filePath = GetRequiredAssemblyPath(assemblyName); + if (!string.IsNullOrEmpty(filePath)) + { + // - In .NET, load the assembly into the custom assembly load context. + return s_proxy.LoadFromAssemblyPath(filePath); + } + } + return null; + //// In .NET Core, PowerShell deals with assembly probing so our logic is much simpler + //// We only care about our Engine assembly + //if (!assemblyName.Name.Equals("PnP.PowerShell.ALC")) + //{ + // return null; + //} + + //// Now load the Engine assembly through the dependency ALC, and let it resolve further dependencies automatically + //return DependencyAssemblyLoadContext.GetForDirectory(s_binCommonPath).LoadFromAssemblyName(assemblyName); + } + + /// + /// Checks to see if the assembly is present in the shared or PSEdition dependencies folder. + /// Check is done by first matching the assembly by its full name; otherwise, we match using the assembly name. + /// + /// to match. + /// True if assembly is present in dependencies folder; otherwise False. + private static bool IsAssemblyPresent(AssemblyName assemblyName) + { + return s_binBasePath.Contains(assemblyName.FullName) || s_binCommonPath.Contains(assemblyName.FullName) + ? true + : !string.IsNullOrEmpty(s_dependencies.SingleOrDefault((x) => x.StartsWith($"{assemblyName.Name},"))) || !string.IsNullOrEmpty(s_psEditionDependencies.SingleOrDefault((x) => x.StartsWith($"{assemblyName.Name},"))); + } + + /// + /// Checks to see if the requested assembly matches the assemblies in our dependencies folder. + /// The requesting assembly is always available in .NET, but could be null in .NET Framework. + /// - When the requesting assembly is available, we check whether the loading request came from this + /// module (the 'Microsoft.Graph*' assembly in this case), so as to make sure we only act on the request + /// from this module. + /// - When the requesting assembly is not available, we just have to depend on the assembly name only. + /// + /// being requested. + /// The requesting . + /// True if assembly is present and matches in dependencies folder; otherwise False. + private static bool IsAssemblyMatching(AssemblyName assemblyName) + { + return assemblyName != null + ? (assemblyName.FullName.StartsWith("Microsoft") || assemblyName.FullName.StartsWith("Azure.Identity")) && IsAssemblyPresent(assemblyName) + : IsAssemblyPresent(assemblyName); + } + + /// + /// Gets the full path of the assembly from the dependencies folder. + /// + /// to find. + /// A representing the full path of the assembly from the dependencies folder; otherwise . + private static string GetRequiredAssemblyPath(AssemblyName assemblyName) + { + string fileName = assemblyName.Name + ".dll"; + string filePath = Path.Combine(s_binBasePath, fileName); + + if (File.Exists(filePath)) + return filePath; + + filePath = Path.Combine(s_binCommonPath, fileName); + return File.Exists(filePath) ? filePath : null; + } + } + + /// + /// An encapsulation of reflection API calls to create a custom AssemblyLoadContext. type is not available when targeting netstandard2.0 .NET Framework. + /// + internal class AssemblyLoadContextProxy + { + private readonly object _customContext; + private readonly MethodInfo _loadFromAssemblyPath; + + private AssemblyLoadContextProxy(Type alc, string loadContextName) + { + var ctor = alc.GetConstructor(new[] { typeof(string), typeof(bool) }); + _loadFromAssemblyPath = alc.GetMethod("LoadFromAssemblyPath", new[] { typeof(string) }); + _customContext = ctor.Invoke(new object[] { loadContextName, false }); + } + + internal Assembly LoadFromAssemblyPath(string assemblyPath) + { + return (Assembly)_loadFromAssemblyPath.Invoke(_customContext, new[] { assemblyPath }); + } + + internal static AssemblyLoadContextProxy CreateLoadContext(string name) + { + if (string.IsNullOrEmpty(name)) { - return null; + throw new ArgumentNullException(nameof(name)); } - // Now load the Engine assembly through the dependency ALC, and let it resolve further dependencies automatically - return DependencyAssemblyLoadContext.GetForDirectory(s_binCommonPath).LoadFromAssemblyName(assemblyName); + var alc = typeof(object).Assembly.GetType("System.Runtime.Loader.AssemblyLoadContext"); + return alc != null + ? new AssemblyLoadContextProxy(alc, name) + : null; } } } From b10ccad564664cd7492d0a7a2fc2d757d5cdf73f Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Thu, 20 Jul 2023 22:23:02 +0300 Subject: [PATCH 2/2] Fix module path load issue --- src/Commands/Base/PnPPowerShellModuleInitializer.cs | 4 ++-- src/Commands/PnP.PowerShell.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/Base/PnPPowerShellModuleInitializer.cs b/src/Commands/Base/PnPPowerShellModuleInitializer.cs index f87b2c1e6..7b034d6fa 100644 --- a/src/Commands/Base/PnPPowerShellModuleInitializer.cs +++ b/src/Commands/Base/PnPPowerShellModuleInitializer.cs @@ -25,8 +25,8 @@ static PnPPowerShellModuleInitializer() s_binBasePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))); s_binCommonPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..", "..", "..", "..", "..", "src", "ALC", "bin", "Debug", "net6.0")); #else - s_binBasePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..")); - s_binCommonPath = Path.Combine(s_binBasePath, "Common"); + s_binBasePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))); + s_binCommonPath = Path.Combine(Path.GetDirectoryName(s_binBasePath), "Common"); #endif s_dependencies = new HashSet(StringComparer.Ordinal); s_psEditionDependencies = new HashSet(StringComparer.Ordinal); diff --git a/src/Commands/PnP.PowerShell.csproj b/src/Commands/PnP.PowerShell.csproj index 9f465b7ae..9fd492c12 100644 --- a/src/Commands/PnP.PowerShell.csproj +++ b/src/Commands/PnP.PowerShell.csproj @@ -46,7 +46,7 @@ TRACE;$(DefineConstants);DEBUG - TRACE;$(DefineConstants);DEBUG + TRACE;$(DefineConstants);Release