Skip to content

Commit

Permalink
Enable cmdlets for Windows PowerShell (microsoft#3951)
Browse files Browse the repository at this point in the history
- Enable the following cmdlets for Windows PowerShell on user context.
  - Find-WinGetPackage
  - Get-WinGetPackage
  - Install-WinGetPackage
  - Uninstall-WinGetPackage
  - Update-WinGetPackage
  - Get-WinGetSource
- Move WinGetAssemblyLoadContext.cs as common code between the modules.
- Implement AppDomain handler for the current AppDomain for module's
dependencies. However, dependency conflicts can still occur. In order to
fix it, one should implement a custom app domain and deal with the
serialization boundaries.
- WinRTHelpers is a static class to handle all native calls. Since
AppDomain doesn't have something similar to LoadUnmanagedDll, a call to
`SetDllDirectoryW` is needed before calling the native method.
###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/3951)
  • Loading branch information
msftrubengu authored Dec 6, 2023
1 parent 1ff0b28 commit e70c3f8
Show file tree
Hide file tree
Showing 15 changed files with 316 additions and 258 deletions.
12 changes: 12 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,18 @@ jobs:
TargetFolder: $(buildOutDirAnyCpu)\PowerShell\Microsoft.WinGet.Client\net6.0-windows10.0.22000.0\SharedDependencies\$(BuildPlatform)
flattenFolders: true

- task: CopyFiles@2
displayName: 'Copy native binaries for Microsoft.WinGet.Client'
inputs:
SourceFolder: $(buildOutDir)
Contents: |
Microsoft.Management.Deployment.InProc\Microsoft.Management.Deployment.dll
Microsoft.Management.Deployment\Microsoft.Management.Deployment.winmd
WindowsPackageManager\WindowsPackageManager.dll
UndockedRegFreeWinRT\winrtact.dll
TargetFolder: $(buildOutDirAnyCpu)\PowerShell\Microsoft.WinGet.Client\net48\SharedDependencies\$(BuildPlatform)
flattenFolders: true

- task: CopyFiles@2
displayName: 'Copy native binaries for Microsoft.WinGet.Configuration'
inputs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// </copyright>
// -----------------------------------------------------------------------------
#if !POWERSHELL_WINDOWS
namespace Microsoft.WinGet.Client.Acl
namespace Microsoft.WinGet.Resolver
{
using System;
using System.Collections.Generic;
Expand All @@ -29,30 +29,29 @@ internal class WinGetAssemblyLoadContext : AssemblyLoadContext
@"WinRT.Runtime.dll",
};

private static readonly string SharedDependencyPath = Path.Combine(
Path.GetDirectoryName(typeof(WinGetAssemblyLoadContext).Assembly.Location),
"SharedDependencies");

private static readonly string DirectDependencyPath = Path.Combine(
Path.GetDirectoryName(typeof(WinGetAssemblyLoadContext).Assembly.Location),
"DirectDependencies");

private static readonly IEnumerable<Architecture> ValidArchs = new Architecture[] { Architecture.X86, Architecture.X64 };
private static readonly string SharedDependencyPath;
private static readonly string SharedArchDependencyPath;
private static readonly string DirectDependencyPath;

private static readonly WinGetAssemblyLoadContext WinGetAcl = new ();

private readonly string sharedArchDependencyPath;
static WinGetAssemblyLoadContext()
{
var self = typeof(WinGetAssemblyLoadContext).Assembly;
SharedDependencyPath = Path.Combine(
Path.GetDirectoryName(self.Location),
"SharedDependencies");
SharedArchDependencyPath = Path.Combine(
SharedDependencyPath,
RuntimeInformation.ProcessArchitecture.ToString().ToLower());
DirectDependencyPath = Path.Combine(
Path.GetDirectoryName(self.Location),
"DirectDependencies");
}

private WinGetAssemblyLoadContext()
: base("WinGetAssemblyLoadContext", isCollectible: false)
{
var arch = RuntimeInformation.ProcessArchitecture;
if (!ValidArchs.Contains(arch))
{
throw new NotSupportedException(arch.ToString());
}

this.sharedArchDependencyPath = Path.Combine(SharedDependencyPath, arch.ToString().ToLower());
}

/// <summary>
Expand All @@ -73,7 +72,7 @@ internal static Assembly ResolvingHandler(AssemblyLoadContext context, AssemblyN
}
}

string path = $"{Path.Combine(DirectDependencyPath, assemblyName.Name)}.dll";
string path = Path.Combine(DirectDependencyPath, name);
if (File.Exists(path))
{
return WinGetAcl.LoadFromAssemblyName(assemblyName);
Expand All @@ -97,7 +96,7 @@ protected override Assembly Load(AssemblyName assemblyName)
return this.LoadFromAssemblyPath(path);
}

path = $"{Path.Combine(this.sharedArchDependencyPath, assemblyName.Name)}.dll";
path = Path.Combine(SharedArchDependencyPath, name);
if (File.Exists(path))
{
return this.LoadFromAssemblyPath(path);
Expand All @@ -115,7 +114,7 @@ protected override Assembly Load(AssemblyName assemblyName)
/// <inheritdoc/>
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string path = Path.Combine(this.sharedArchDependencyPath, unmanagedDllName);
string path = Path.Combine(SharedArchDependencyPath, unmanagedDllName);
if (File.Exists(path))
{
return this.LoadUnmanagedDllFromPath(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\CommonFiles\WinGetAssemblyLoadContext.cs" Link="WinGetAssemblyLoadContext.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
Expand Down Expand Up @@ -66,16 +70,11 @@
<PowerShellModuleRuntimesDir>$(PowerShellModuleOutputDirectory)\$(TargetFramework)</PowerShellModuleRuntimesDir>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == '$(CoreFramework)'">
<PropertyGroup>
<PowerShellModuleSharedDependencies>$(PowerShellModuleRuntimesDir)\SharedDependencies</PowerShellModuleSharedDependencies>
<PowerShellModuleDirectDependencies>$(PowerShellModuleRuntimesDir)\DirectDependencies</PowerShellModuleDirectDependencies>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == '$(DesktopFramework)'">
<PowerShellModuleSharedDependencies>$(PowerShellModuleRuntimesDir)</PowerShellModuleSharedDependencies>
<PowerShellModuleDirectDependencies>$(PowerShellModuleRuntimesDir)</PowerShellModuleDirectDependencies>
</PropertyGroup>

<!-- psd1 and ps1xml-->
<Target Name="CopyModuleFiles" AfterTargets="AfterBuild">
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// -----------------------------------------------------------------------------
// <copyright file="ModuleInit.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace Microsoft.WinGet.Resolver
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Runtime.InteropServices;

#if !POWERSHELL_WINDOWS
using System.Runtime.Loader;
#else
using Microsoft.WinGet.Client.Cmdlets.Resolver;
#endif

/// <summary>
/// Initialization class for this module.
/// </summary>
public class ModuleInit : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
private static readonly IEnumerable<Architecture> ValidArchs = new Architecture[] { Architecture.X86, Architecture.X64 };

/// <inheritdoc/>
public void OnImport()
{
var arch = RuntimeInformation.ProcessArchitecture;
if (!ValidArchs.Contains(arch))
{
throw new NotSupportedException(arch.ToString());
}

#if !POWERSHELL_WINDOWS
AssemblyLoadContext.Default.Resolving += WinGetAssemblyLoadContext.ResolvingHandler;
#else
// If we really need to avoid dependency conflicts, we could create a custom domain and handle the serialization boundaries.
// PowerShell doesn't recommended because its complications.
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += WinGetAppDomain.Handler;
#endif
}

/// <inheritdoc/>
public void OnRemove(PSModuleInfo module)
{
#if !POWERSHELL_WINDOWS
AssemblyLoadContext.Default.Resolving -= WinGetAssemblyLoadContext.ResolvingHandler;
#else
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve -= WinGetAppDomain.Handler;
#endif
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// -----------------------------------------------------------------------------
// <copyright file="WinGetAppDomain.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------
#if POWERSHELL_WINDOWS

namespace Microsoft.WinGet.Client.Cmdlets.Resolver
{
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

/// <summary>
/// Resolver for assemblies.
/// </summary>
internal static class WinGetAppDomain
{
private static readonly string SharedDependencyPath;
private static readonly string SharedArchDependencyPath;
private static readonly string DirectDependencyPath;

static WinGetAppDomain()
{
var self = typeof(WinGetAppDomain).Assembly;
SharedDependencyPath = Path.Combine(
Path.GetDirectoryName(self.Location),
"SharedDependencies");
SharedArchDependencyPath = Path.Combine(
SharedDependencyPath,
RuntimeInformation.ProcessArchitecture.ToString().ToLower());
DirectDependencyPath = Path.Combine(
Path.GetDirectoryName(self.Location),
"DirectDependencies");
}

/// <summary>
/// Handler to register in the AppDomain.
/// </summary>
/// <param name="source">Source.</param>
/// <param name="args">Event args.</param>
/// <returns>The assembly if found.</returns>
public static Assembly Handler(object source, ResolveEventArgs args)
{
string name = $"{new AssemblyName(args.Name).Name}.dll";
string path = Path.Combine(SharedDependencyPath, name);
if (File.Exists(path))
{
return Assembly.LoadFile(path);
}

path = Path.Combine(SharedArchDependencyPath, name);
if (File.Exists(path))
{
return Assembly.LoadFile(path);
}

path = Path.Combine(DirectDependencyPath, name);
if (File.Exists(path))
{
return Assembly.LoadFile(path);
}

return null;
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Management.Deployment;
using Microsoft.WinGet.Client.Engine.Common;
Expand All @@ -23,9 +22,7 @@ public abstract class ManagementDeploymentCommand : BaseCommand
{
static ManagementDeploymentCommand()
{
#if !POWERSHELL_WINDOWS
InitializeUndockedRegFreeWinRT();
#endif
WinRTHelpers.Initialize();
}

/// <summary>
Expand All @@ -36,7 +33,10 @@ internal ManagementDeploymentCommand(PSCmdlet psCmdlet)
: base(psCmdlet)
{
#if POWERSHELL_WINDOWS
throw new WindowsPowerShellNotSupported();
if (Utilities.UsesInProcWinget)
{
throw new WindowsPowerShellNotSupported();
}
#endif
}

Expand Down Expand Up @@ -96,8 +96,5 @@ protected IReadOnlyList<PackageCatalogReference> GetPackageCatalogReferences(str
};
}
}

[DllImport("winrtact.dll", EntryPoint = "winrtact_Initialize", ExactSpelling = true, PreserveSig = true)]
private static extern void InitializeUndockedRegFreeWinRT();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ namespace Microsoft.WinGet.Client.Engine.Helpers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Management.Deployment;
using Microsoft.WinGet.Client.Engine.Common;
Expand Down Expand Up @@ -167,32 +164,18 @@ private static T Create<T>(Type? type, in Guid iid)

if (Utilities.UsesInProcWinget)
{
var arch = RuntimeInformation.ProcessArchitecture;
if (!ValidArchs.Contains(arch))
{
throw new NotSupportedException(arch.ToString());
}

string executingAssemblyLocation = Assembly.GetExecutingAssembly().Location;
string executingAssemblyDirectory = Path.Combine(Path.GetDirectoryName(executingAssemblyLocation) !, arch.ToString().ToLower());

SetDllDirectoryW(executingAssemblyDirectory);

try
{
return new T();
}
finally
{
SetDllDirectoryW(null);
}
// This doesn't work on Windows PowerShell
// If we want to support it, we need something that loads the
// Microsoft.Management.Deployment.dll for .NET framework as CsWinRT
// does for .NET Core
return new T();
}

object? instance = null;

if (Utilities.ExecutingAsAdministrator)
{
int hr = WinGetServerManualActivation_CreateInstance(type.GUID, iid, 0, out instance);
int hr = WinRTHelpers.ManualActivation(type.GUID, iid, 0, out instance);

if (hr < 0)
{
Expand Down Expand Up @@ -223,16 +206,5 @@ private static T Create<T>(Type? type, in Guid iid)
return (T)instance;
#endif
}

[DllImport("winrtact.dll", EntryPoint = "WinGetServerManualActivation_CreateInstance", ExactSpelling = true, PreserveSig = true)]
private static extern int WinGetServerManualActivation_CreateInstance(
[In, MarshalAs(UnmanagedType.LPStruct)] Guid clsid,
[In, MarshalAs(UnmanagedType.LPStruct)] Guid iid,
uint flags,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object instance);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetDllDirectoryW([MarshalAs(UnmanagedType.LPWStr)] string? directory);
}
}
Loading

0 comments on commit e70c3f8

Please sign in to comment.