Skip to content

Commit

Permalink
Add dependency resolver based on Microsoft.Extensions.DependencyModel (
Browse files Browse the repository at this point in the history
  • Loading branch information
bording authored Dec 19, 2023
1 parent 867d7ee commit add2b8b
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageVersion Include="Microsoft.AspNet.WebApi.OwinSelfHost" Version="5.3.0" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="2.1.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
Expand Down
111 changes: 111 additions & 0 deletions src/ServiceControl.Management.PowerShell/DependencyResolver.cs
Original file line number Diff line number Diff line change
@@ -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<RuntimeAssetGroup> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<!--Ensures that all project references are explicitly defined in this file -->
<DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
<LangVersion>12.0</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand All @@ -26,11 +27,13 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyModel" GeneratePathProperty="true" />
<PackageReference Include="System.Management.Automation" />
</ItemGroup>

<ItemGroup>
<Artifact Include="$(OutputPath)" DestinationFolder="$(PowerShellModuleArtifactsPath)" />
<Artifact Include="$(PkgMicrosoft_Extensions_DependencyModel)\lib\netstandard2.0\Microsoft.Extensions.DependencyModel.dll" DestinationFolder="$(PowerShellModuleArtifactsPath)" />
<Artifact Include="$(PowerShellModuleName).psd1" DestinationFolder="$(PowerShellModuleArtifactsPath)" />
<Artifact Include="$(PowerShellModuleName).psm1" DestinationFolder="$(PowerShellModuleArtifactsPath)" />
<Artifact Include="$(PowerShellModuleName).format.ps1xml" DestinationFolder="$(PowerShellModuleArtifactsPath)" />
Expand Down

0 comments on commit add2b8b

Please sign in to comment.