From add2b8be40564ca155007c5157ed3dcd86135bf3 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Tue, 19 Dec 2023 14:51:12 -0500 Subject: [PATCH] Add dependency resolver based on Microsoft.Extensions.DependencyModel (#3901) --- src/Directory.Packages.props | 1 + .../DependencyResolver.cs | 111 ++++++++++++++++++ .../InstallerEngineAssemblyLoadContext.cs | 59 +++++----- .../ModuleAssemblyInitializer.cs | 12 +- ...erviceControl.Management.PowerShell.csproj | 3 + 5 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 src/ServiceControl.Management.PowerShell/DependencyResolver.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 0bab4f3437..461b6631e7 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -22,6 +22,7 @@ + diff --git a/src/ServiceControl.Management.PowerShell/DependencyResolver.cs b/src/ServiceControl.Management.PowerShell/DependencyResolver.cs new file mode 100644 index 0000000000..2423da6e96 --- /dev/null +++ b/src/ServiceControl.Management.PowerShell/DependencyResolver.cs @@ -0,0 +1,111 @@ +#nullable enable +namespace ServiceControl.Management.PowerShell; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyModel; + +class DependencyResolver +{ + readonly string assemblyDirectory; + readonly DependencyContext dependencyContext; + readonly string?[] runtimes; + + public DependencyResolver(string assemblyPath) + { + assemblyDirectory = Path.GetDirectoryName(assemblyPath) ?? string.Empty; + + var depsJsonFile = Path.ChangeExtension(assemblyPath, "deps.json"); + using var fileStream = File.OpenRead(depsJsonFile); + + var reader = new DependencyContextJsonReader(); + dependencyContext = reader.Read(fileStream); + + var runtimeGraph = DependencyContext.Default?.RuntimeGraph.SingleOrDefault(r => r.Runtime.Equals(RuntimeInformation.RuntimeIdentifier, StringComparison.Ordinal)); + + // PowerShell is still building against OS-specific RIDs on Windows, so the expected runtime graph information isn't in the default deps.json file. + // Try looking it up again using the RID specified in the deps.json file instead. + runtimeGraph ??= DependencyContext.Default?.RuntimeGraph.SingleOrDefault(r => r.Runtime.Equals(DependencyContext.Default.Target.Runtime, StringComparison.Ordinal)); + + if (runtimeGraph is not null) + { + runtimes = [runtimeGraph.Runtime, .. runtimeGraph.Fallbacks, string.Empty]; + } + else // Runtime graph information isn't available when running in WinRM, so assume we're running on Windows if null at this point + { + runtimes = [Environment.Is64BitProcess ? "win-x64" : "win-x86", "win", "any", "base", string.Empty]; + } + } + + public string? ResolveAssemblyToPath(AssemblyName assemblyName) + { + ArgumentNullException.ThrowIfNull(assemblyName); + + var library = dependencyContext.RuntimeLibraries.SingleOrDefault(r => r.Name.Equals(assemblyName.Name, StringComparison.Ordinal)); + + if (library is null) + { + return null; + } + + // If we had dependencies that had satellite resource assemblies, we'd need to use assemblyName.CultureName and library.ResourceAssemblies instead + + return SearchRuntimeAssets(library.RuntimeAssemblyGroups); + } + + public string? ResolveUnmanagedDllToPath(string unmanagedDllName) + { + //This logic is good enough for our purposes, but is not comprehensive enough for general, cross-platform use. + var nativeLibraryGroups = dependencyContext.RuntimeLibraries.SelectMany(r => r.NativeLibraryGroups); + var candidateGroups = nativeLibraryGroups.Where(r => r.AssetPaths[0].Contains(unmanagedDllName, StringComparison.OrdinalIgnoreCase)); + + return SearchRuntimeAssets(candidateGroups); + } + + string? SearchRuntimeAssets(IEnumerable runtimeAssets) + { + if (!runtimeAssets.Any()) + { + return null; + } + + string? assetPath = null; + + foreach (var runtime in runtimes) + { + foreach (var asset in runtimeAssets) + { + if (asset.Runtime.Equals(runtime, StringComparison.Ordinal)) + { + assetPath = asset.AssetPaths[0]; + break; + } + } + + if (assetPath is not null) + { + // This assumes that we're running with all assemblies copied locally, and doesn't attempt to cover the scenario of needing to resolve + // from the NuGet package cache folder. + if (assetPath.StartsWith("lib", StringComparison.Ordinal)) + { + assetPath = Path.GetFileName(assetPath); + } + + assetPath = Path.GetFullPath(Path.Combine(assemblyDirectory, assetPath)); + + if (!File.Exists(assetPath)) + { + assetPath = null; + } + + break; + } + } + + return assetPath; + } +} diff --git a/src/ServiceControl.Management.PowerShell/InstallerEngineAssemblyLoadContext.cs b/src/ServiceControl.Management.PowerShell/InstallerEngineAssemblyLoadContext.cs index 1468f64761..9cc439efec 100644 --- a/src/ServiceControl.Management.PowerShell/InstallerEngineAssemblyLoadContext.cs +++ b/src/ServiceControl.Management.PowerShell/InstallerEngineAssemblyLoadContext.cs @@ -1,44 +1,43 @@ -namespace ServiceControl.Management.PowerShell +namespace ServiceControl.Management.PowerShell; + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; + +class InstallerEngineAssemblyLoadContext : AssemblyLoadContext { - using System; - using System.IO; - using System.Reflection; - using System.Runtime.Loader; + readonly DependencyResolver resolver; - class InstallerEngineAssemblyLoadContext : AssemblyLoadContext + public InstallerEngineAssemblyLoadContext() { - readonly AssemblyDependencyResolver resolver; + var executingAssemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var assemblyPath = Path.Combine(executingAssemblyDirectory, "InstallerEngine", "ServiceControlInstaller.Engine.dll"); - public InstallerEngineAssemblyLoadContext() - { - var executingAssemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - var assemblyPath = Path.Combine(executingAssemblyDirectory, "InstallerEngine", "ServiceControlInstaller.Engine.dll"); + resolver = new DependencyResolver(assemblyPath); + } - resolver = new AssemblyDependencyResolver(assemblyPath); - } + protected override Assembly Load(AssemblyName assemblyName) + { + var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName); - protected override Assembly Load(AssemblyName assemblyName) + if (assemblyPath != null) { - var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName); - - if (assemblyPath != null) - { - return LoadFromAssemblyPath(assemblyPath); - } - - return null; + return LoadFromAssemblyPath(assemblyPath); } - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - var unmanagedDllPath = resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + return null; + } - if (unmanagedDllPath != null) - { - return LoadUnmanagedDllFromPath(unmanagedDllPath); - } + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var unmanagedDllPath = resolver.ResolveUnmanagedDllToPath(unmanagedDllName); - return IntPtr.Zero; + if (unmanagedDllPath != null) + { + return LoadUnmanagedDllFromPath(unmanagedDllPath); } + + return IntPtr.Zero; } } diff --git a/src/ServiceControl.Management.PowerShell/ModuleAssemblyInitializer.cs b/src/ServiceControl.Management.PowerShell/ModuleAssemblyInitializer.cs index e2b8cbca04..3f323e0a20 100644 --- a/src/ServiceControl.Management.PowerShell/ModuleAssemblyInitializer.cs +++ b/src/ServiceControl.Management.PowerShell/ModuleAssemblyInitializer.cs @@ -12,6 +12,16 @@ public class ModuleAssemblyInitializer : IModuleAssemblyInitializer, IModuleAsse public void OnRemove(PSModuleInfo psModuleInfo) => AssemblyLoadContext.Default.Resolving -= Resolve; - static Assembly Resolve(AssemblyLoadContext defaultLoadContext, AssemblyName assemblyName) => installerEngineLoadContext.LoadFromAssemblyName(assemblyName); + static Assembly Resolve(AssemblyLoadContext defaultLoadContext, AssemblyName assemblyName) + { + if (assemblyName.Name.Contains("ServiceControlInstaller.Engine")) + { + return installerEngineLoadContext.LoadFromAssemblyName(assemblyName); + } + else + { + return null; + } + } } } diff --git a/src/ServiceControl.Management.PowerShell/ServiceControl.Management.PowerShell.csproj b/src/ServiceControl.Management.PowerShell/ServiceControl.Management.PowerShell.csproj index aa49af5020..a76fbf87ef 100644 --- a/src/ServiceControl.Management.PowerShell/ServiceControl.Management.PowerShell.csproj +++ b/src/ServiceControl.Management.PowerShell/ServiceControl.Management.PowerShell.csproj @@ -17,6 +17,7 @@ true + 12.0 @@ -26,11 +27,13 @@ + +