Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #3207 - issue with colliding assemblies #3280

Merged
merged 3 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 57 additions & 57 deletions src/Commands/Base/DependencyAssemblyLoadContext.cs
Original file line number Diff line number Diff line change
@@ -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<string, DependencyAssemblyLoadContext> s_dependencyLoadContexts = new ConcurrentDictionary<string, DependencyAssemblyLoadContext>();

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<string, DependencyAssemblyLoadContext> s_dependencyLoadContexts = new ConcurrentDictionary<string, DependencyAssemblyLoadContext>();

// 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;
// }
// }
//}
157 changes: 148 additions & 9 deletions src/Commands/Base/PnPPowerShellModuleInitializer.cs
Original file line number Diff line number Diff line change
@@ -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<string> s_dependencies;
private static readonly HashSet<string> 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(Path.GetDirectoryName(s_binBasePath), "Common");
#endif
s_dependencies = new HashSet<string>(StringComparer.Ordinal);
s_psEditionDependencies = new HashSet<string>(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()
{
Expand All @@ -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);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="assemblyName"><see cref="AssemblyName"/> to match.</param>
/// <returns>True if assembly is present in dependencies folder; otherwise False.</returns>
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},")));
}

/// <summary>
/// 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.
/// </summary>
/// <param name="assemblyName"><see cref="AssemblyName"/> being requested.</param>
/// <param name="requestingAssembly">The requesting <see cref="Assembly"/>.</param>
/// <returns>True if assembly is present and matches in dependencies folder; otherwise False.</returns>
private static bool IsAssemblyMatching(AssemblyName assemblyName)
{
return assemblyName != null
? (assemblyName.FullName.StartsWith("Microsoft") || assemblyName.FullName.StartsWith("Azure.Identity")) && IsAssemblyPresent(assemblyName)
: IsAssemblyPresent(assemblyName);
}

/// <summary>
/// Gets the full path of the assembly from the dependencies folder.
/// </summary>
/// <param name="assemblyName"><see cref="AssemblyName"/> to find.</param>
/// <returns>A <see cref="string"/> representing the full path of the assembly from the dependencies folder; otherwise <see cref="null"/>.</returns>
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;
}
}

/// <summary>
/// An encapsulation of reflection API calls to create a custom AssemblyLoadContext. <see cref="AssemblyLoadContext"/> type is not available when targeting netstandard2.0 .NET Framework.
/// </summary>
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;
}
}
}
2 changes: 1 addition & 1 deletion src/Commands/PnP.PowerShell.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<DefineConstants>TRACE;$(DefineConstants);DEBUG</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
<DefineConstants>TRACE;$(DefineConstants);DEBUG</DefineConstants>
<DefineConstants>TRACE;$(DefineConstants);Release</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\parker.ico" />
Expand Down
Loading