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

PowerShell module does not work when used with WinRM remoting #3901

Merged
merged 1 commit into from
Dec 19, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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