commit e70c3f8a159d8d98900a5313e344462a3ee67d74 parent 1ff0b2832cd465ba66acab00b8c6f86c2010d4de Author: Ruben Guerrero <rubengu@microsoft.com> Date: Wed, 6 Dec 2023 11:03:00 -0800 Enable cmdlets for Windows PowerShell (#3951) - 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) Diffstat:
17 files changed, 450 insertions(+), 393 deletions(-)
diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -370,6 +370,18 @@ jobs: 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: SourceFolder: $(buildOutDir) diff --git a/src/PowerShell/CommonFiles/WinGetAssemblyLoadContext.cs b/src/PowerShell/CommonFiles/WinGetAssemblyLoadContext.cs @@ -0,0 +1,127 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinGetAssemblyLoadContext.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- +#if !POWERSHELL_WINDOWS +namespace Microsoft.WinGet.Resolver +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Runtime.InteropServices; + using System.Runtime.Loader; + + /// <summary> + /// Custom assembly load context for this module. + /// This helps us load our dependencies without carrying about apps importing this module. + /// All dependencies except the Engine dll needs to be under a Dependencies directory. + /// </summary> + internal class WinGetAssemblyLoadContext : AssemblyLoadContext + { + // The assemblies must be loaded in the default context. + // Loading WinRT.Runtime.dll in an ALC when is already loaded in the default context + // will result on 'Attempt to update previously set global instance.' + private static readonly IEnumerable<string> DefaultContextAssemblies = new string[] + { + @"WinRT.Runtime.dll", + }; + + private static readonly string SharedDependencyPath; + private static readonly string SharedArchDependencyPath; + private static readonly string DirectDependencyPath; + + private static readonly WinGetAssemblyLoadContext WinGetAcl = new (); + + 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) + { + } + + /// <summary> + /// Handler to resolve assemblies. + /// </summary> + /// <param name="context">Assembly load context.</param> + /// <param name="assemblyName">Assembly name.</param> + /// <returns>The assembly, null if not in our assembly location.</returns> + internal static Assembly ResolvingHandler(AssemblyLoadContext context, AssemblyName assemblyName) + { + string name = $"{assemblyName.Name}.dll"; + if (DefaultContextAssemblies.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + string sharedPath = Path.Combine(SharedDependencyPath, name); + if (File.Exists(sharedPath)) + { + return AssemblyLoadContext.Default.LoadFromAssemblyPath(sharedPath); + } + } + + string path = Path.Combine(DirectDependencyPath, name); + if (File.Exists(path)) + { + return WinGetAcl.LoadFromAssemblyName(assemblyName); + } + + return null; + } + + /// <inheritdoc/> + protected override Assembly Load(AssemblyName assemblyName) + { + string name = $"{assemblyName.Name}.dll"; + if (DefaultContextAssemblies.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + return null; + } + + string path = Path.Combine(SharedDependencyPath, name); + if (File.Exists(path)) + { + return this.LoadFromAssemblyPath(path); + } + + path = Path.Combine(SharedArchDependencyPath, name); + if (File.Exists(path)) + { + return this.LoadFromAssemblyPath(path); + } + + path = Path.Combine(DirectDependencyPath, name); + if (File.Exists(path)) + { + return this.LoadFromAssemblyPath(path); + } + + return null; + } + + /// <inheritdoc/> + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + string path = Path.Combine(SharedArchDependencyPath, unmanagedDllName); + if (File.Exists(path)) + { + return this.LoadUnmanagedDllFromPath(path); + } + + return IntPtr.Zero; + } + } +} +#endif diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Acl/ModuleInit.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Acl/ModuleInit.cs @@ -1,30 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="ModuleInit.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- -#if !POWERSHELL_WINDOWS -namespace Microsoft.WinGet.Client.Acl -{ - using System.Management.Automation; - using System.Runtime.Loader; - - /// <summary> - /// Initialization class for this module. - /// </summary> - public class ModuleInit : IModuleAssemblyInitializer, IModuleAssemblyCleanup - { - /// <inheritdoc/> - public void OnImport() - { - AssemblyLoadContext.Default.Resolving += WinGetAssemblyLoadContext.ResolvingHandler; - } - - /// <inheritdoc/> - public void OnRemove(PSModuleInfo module) - { - AssemblyLoadContext.Default.Resolving -= WinGetAssemblyLoadContext.ResolvingHandler; - } - } -} -#endif- \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Acl/WinGetAssemblyLoadContext.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Acl/WinGetAssemblyLoadContext.cs @@ -1,128 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="WinGetAssemblyLoadContext.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- -#if !POWERSHELL_WINDOWS -namespace Microsoft.WinGet.Client.Acl -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Runtime.InteropServices; - using System.Runtime.Loader; - - /// <summary> - /// Custom assembly load context for this module. - /// This helps us load our dependencies without carrying about apps importing this module. - /// All dependencies except the Engine dll needs to be under a Dependencies directory. - /// </summary> - internal class WinGetAssemblyLoadContext : AssemblyLoadContext - { - // The assemblies must be loaded in the default context. - // Loading WinRT.Runtime.dll in an ALC when is already loaded in the default context - // will result on 'Attempt to update previously set global instance.' - private static readonly IEnumerable<string> DefaultContextAssemblies = new string[] - { - @"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 WinGetAssemblyLoadContext WinGetAcl = new (); - - private readonly string sharedArchDependencyPath; - - 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> - /// Handler to resolve assemblies. - /// </summary> - /// <param name="context">Assembly load context.</param> - /// <param name="assemblyName">Assembly name.</param> - /// <returns>The assembly, null if not in our assembly location.</returns> - internal static Assembly ResolvingHandler(AssemblyLoadContext context, AssemblyName assemblyName) - { - string name = $"{assemblyName.Name}.dll"; - if (DefaultContextAssemblies.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - string sharedPath = Path.Combine(SharedDependencyPath, name); - if (File.Exists(sharedPath)) - { - return AssemblyLoadContext.Default.LoadFromAssemblyPath(sharedPath); - } - } - - string path = $"{Path.Combine(DirectDependencyPath, assemblyName.Name)}.dll"; - if (File.Exists(path)) - { - return WinGetAcl.LoadFromAssemblyName(assemblyName); - } - - return null; - } - - /// <inheritdoc/> - protected override Assembly Load(AssemblyName assemblyName) - { - string name = $"{assemblyName.Name}.dll"; - if (DefaultContextAssemblies.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - return null; - } - - string path = Path.Combine(SharedDependencyPath, name); - if (File.Exists(path)) - { - return this.LoadFromAssemblyPath(path); - } - - path = $"{Path.Combine(this.sharedArchDependencyPath, assemblyName.Name)}.dll"; - if (File.Exists(path)) - { - return this.LoadFromAssemblyPath(path); - } - - path = Path.Combine(DirectDependencyPath, name); - if (File.Exists(path)) - { - return this.LoadFromAssemblyPath(path); - } - - return null; - } - - /// <inheritdoc/> - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - string path = Path.Combine(this.sharedArchDependencyPath, unmanagedDllName); - if (File.Exists(path)) - { - return this.LoadUnmanagedDllFromPath(path); - } - - return IntPtr.Zero; - } - } -} -#endif diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Microsoft.WinGet.Client.Cmdlets.csproj b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Microsoft.WinGet.Client.Cmdlets.csproj @@ -31,6 +31,10 @@ </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"> <PrivateAssets>all</PrivateAssets> @@ -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> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Resolver/ModuleInit.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Resolver/ModuleInit.cs @@ -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 + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Resolver/WinGetAppDomain.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Resolver/WinGetAppDomain.cs @@ -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 diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/ManagementDeploymentCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/ManagementDeploymentCommand.cs @@ -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; @@ -23,9 +22,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common { static ManagementDeploymentCommand() { -#if !POWERSHELL_WINDOWS - InitializeUndockedRegFreeWinRT(); -#endif + WinRTHelpers.Initialize(); } /// <summary> @@ -36,7 +33,10 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common : base(psCmdlet) { #if POWERSHELL_WINDOWS - throw new WindowsPowerShellNotSupported(); + if (Utilities.UsesInProcWinget) + { + throw new WindowsPowerShellNotSupported(); + } #endif } @@ -96,8 +96,5 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common }; } } - - [DllImport("winrtact.dll", EntryPoint = "winrtact_Initialize", ExactSpelling = true, PreserveSig = true)] - private static extern void InitializeUndockedRegFreeWinRT(); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs @@ -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; @@ -167,32 +164,18 @@ namespace Microsoft.WinGet.Client.Engine.Helpers 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) { @@ -223,16 +206,5 @@ namespace Microsoft.WinGet.Client.Engine.Helpers 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); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinRTHelpers.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinRTHelpers.cs @@ -0,0 +1,94 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinRTHelpers.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.IO; + using System.Runtime.InteropServices; + + /// <summary> + /// Helper class for winrtact.dll calls. + /// </summary> + internal static class WinRTHelpers + { +#if POWERSHELL_WINDOWS + private static readonly string ArchDependencyPath; + + static WinRTHelpers() + { + ArchDependencyPath = Path.Combine( + Path.GetDirectoryName( + Path.GetDirectoryName(typeof(WinRTHelpers).Assembly.Location)), + "SharedDependencies", + RuntimeInformation.ProcessArchitecture.ToString().ToLower()); + } +#endif + + /// <summary> + /// Calls winrtact_Initialize. + /// </summary> + public static void Initialize() + { +#if POWERSHELL_WINDOWS + SetDllDirectoryW(ArchDependencyPath); + + try + { +#endif + InitializeUndockedRegFreeWinRT(); + +#if POWERSHELL_WINDOWS + } + finally + { + SetDllDirectoryW(null); + } +#endif + } + + /// <summary> + /// Calls WinGetServerManualActivation_CreateInstance. + /// </summary> + /// <param name="clsid">Class id.</param> + /// <param name="iid">IID.</param> + /// <param name="flags">Flags.</param> + /// <param name="instance">Out object.</param> + /// <returns>Result of WinGetServerManualActivation_CreateInstance.</returns> + public static int ManualActivation(Guid clsid, Guid iid, uint flags, out object instance) + { +#if POWERSHELL_WINDOWS + SetDllDirectoryW(ArchDependencyPath); + + try + { +#endif + return WinGetServerManualActivation_CreateInstance(clsid, iid, flags, out instance); + +#if POWERSHELL_WINDOWS + } + finally + { + SetDllDirectoryW(null); + } +#endif + } + + [DllImport("winrtact.dll", EntryPoint = "winrtact_Initialize", ExactSpelling = true, PreserveSig = true)] + private static extern void InitializeUndockedRegFreeWinRT(); + + [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); + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs @@ -7,7 +7,6 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System.Linq; - using System.Management.Automation; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Exceptions; @@ -61,11 +60,11 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects /// <summary> /// Gets the source name of the catalog package. /// </summary> - public string Source + public string? Source { get { - return this.CatalogPackageCOM.DefaultInstallVersion.PackageCatalog.Info.Name; + return this.CatalogPackageCOM.DefaultInstallVersion?.PackageCatalog.Info.Name; } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Acl/CustomAssemblyLoadContext.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Acl/CustomAssemblyLoadContext.cs @@ -1,127 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="CustomAssemblyLoadContext.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- -namespace Microsoft.WinGet.Configuration.Acl -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Runtime.InteropServices; - using System.Runtime.Loader; - - /// <summary> - /// Custom assembly load context for this module. - /// This helps us load our dependencies without carrying about apps importing this module. - /// All dependencies except the Engine dll needs to be under a Dependencies directory. - /// </summary> - internal class CustomAssemblyLoadContext : AssemblyLoadContext - { - // The assemblies must be loaded in the default context. - // Loading WinRT.Runtime.dll in an ALC when is already loaded in the default context - // will result on 'Attempt to update previously set global instance.' - private static readonly IEnumerable<string> DefaultContextAssemblies = new string[] - { - @"WinRT.Runtime.dll", - }; - - private static readonly string SharedDependencyPath = Path.Combine( - Path.GetDirectoryName(typeof(CustomAssemblyLoadContext).Assembly.Location), - "SharedDependencies"); - - private static readonly string DirectDependencyPath = Path.Combine( - Path.GetDirectoryName(typeof(CustomAssemblyLoadContext).Assembly.Location), - "DirectDependencies"); - - private static readonly IEnumerable<Architecture> ValidArchs = new Architecture[] { Architecture.X86, Architecture.X64, Architecture.Arm64 }; - - private static readonly CustomAssemblyLoadContext WinGetAcl = new (); - - private readonly string sharedArchDependencyPath; - - private CustomAssemblyLoadContext() - : 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> - /// Handler to resolve assemblies. - /// </summary> - /// <param name="context">Assembly load context.</param> - /// <param name="assemblyName">Assembly name.</param> - /// <returns>The assembly, null if not in our assembly location.</returns> - internal static Assembly ResolvingHandler(AssemblyLoadContext context, AssemblyName assemblyName) - { - string name = $"{assemblyName.Name}.dll"; - if (DefaultContextAssemblies.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - string sharedPath = Path.Combine(SharedDependencyPath, name); - if (File.Exists(sharedPath)) - { - return AssemblyLoadContext.Default.LoadFromAssemblyPath(sharedPath); - } - } - - string path = $"{Path.Combine(DirectDependencyPath, assemblyName.Name)}.dll"; - if (File.Exists(path)) - { - return WinGetAcl.LoadFromAssemblyName(assemblyName); - } - - return null; - } - - /// <inheritdoc/> - protected override Assembly Load(AssemblyName assemblyName) - { - string name = $"{assemblyName.Name}.dll"; - if (DefaultContextAssemblies.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - return null; - } - - string path = $"{Path.Combine(SharedDependencyPath, assemblyName.Name)}.dll"; - if (File.Exists(path)) - { - return this.LoadFromAssemblyPath(path); - } - - path = $"{Path.Combine(this.sharedArchDependencyPath, assemblyName.Name)}.dll"; - if (File.Exists(path)) - { - return this.LoadFromAssemblyPath(path); - } - - path = $"{Path.Combine(DirectDependencyPath, assemblyName.Name)}.dll"; - if (File.Exists(path)) - { - return this.LoadFromAssemblyPath(path); - } - - return null; - } - - /// <inheritdoc/> - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - string path = Path.Combine(SharedDependencyPath, unmanagedDllName); - if (File.Exists(path)) - { - return this.LoadUnmanagedDllFromPath(path); - } - - return IntPtr.Zero; - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Acl/ModuleInit.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Acl/ModuleInit.cs @@ -1,29 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="ModuleInit.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Configuration.Acl -{ - using System.Management.Automation; - using System.Runtime.Loader; - - /// <summary> - /// Initialization class for this module. - /// </summary> - public class ModuleInit : IModuleAssemblyInitializer, IModuleAssemblyCleanup - { - /// <inheritdoc/> - public void OnImport() - { - AssemblyLoadContext.Default.Resolving += CustomAssemblyLoadContext.ResolvingHandler; - } - - /// <inheritdoc/> - public void OnRemove(PSModuleInfo module) - { - AssemblyLoadContext.Default.Resolving -= CustomAssemblyLoadContext.ResolvingHandler; - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Microsoft.WinGet.Configuration.Cmdlets.csproj b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Microsoft.WinGet.Configuration.Cmdlets.csproj @@ -26,6 +26,10 @@ </ItemGroup> <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"> <PrivateAssets>all</PrivateAssets> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Resolver/ModuleInit.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Resolver/ModuleInit.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------------- +// <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; + using System.Runtime.Loader; + + /// <summary> + /// Initialization class for this module. + /// </summary> + public class ModuleInit : IModuleAssemblyInitializer, IModuleAssemblyCleanup + { + private static readonly IEnumerable<Architecture> ValidArchs = new Architecture[] { Architecture.X86, Architecture.X64, Architecture.Arm64 }; + + /// <inheritdoc/> + public void OnImport() + { + var arch = RuntimeInformation.ProcessArchitecture; + if (!ValidArchs.Contains(arch)) + { + throw new NotSupportedException(arch.ToString()); + } + + AssemblyLoadContext.Default.Resolving += WinGetAssemblyLoadContext.ResolvingHandler; + } + + /// <inheritdoc/> + public void OnRemove(PSModuleInfo module) + { + AssemblyLoadContext.Default.Resolving -= WinGetAssemblyLoadContext.ResolvingHandler; + } + } +} diff --git a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -32,7 +32,10 @@ param ( $BuildRoot = "", [switch] - $SkipImportModule + $SkipImportModule, + + [switch] + $Clean ) class WinGetModule @@ -161,6 +164,11 @@ if ($BuildRoot -eq "") $BuildRoot = "$PSScriptRoot\..\.."; } +if ($Clean -and (Test-Path $moduleRootOutput)) +{ + Remove-Item $moduleRootOutput -Recurse +} + # Modules, they should be in dependency order so that when importing we don't pick up the release modules. $local:modules = @() if ($moduleToConfigure.HasFlag([ModuleType]::Client)) @@ -176,6 +184,7 @@ if ($moduleToConfigure.HasFlag([ModuleType]::Client)) "UndockedRegFreeWinRT\winrtact.dll" ) $module.AddArchSpecificFiles($additionalFiles, "net6.0-windows10.0.22000.0\SharedDependencies", $BuildRoot, $Configuration) + $module.AddArchSpecificFiles($additionalFiles, "net48\SharedDependencies", $BuildRoot, $Configuration) $modules += $module } diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -46,19 +46,22 @@ BeforeAll { } } + # This is a workaround to an issue where the server takes longer than expected to terminate when + # running from PowerShell. This can cause other E2E tests to fail when attempting to reset the test source. function RemoveTestSource { - if ($PSEdition -eq "Core") - { - try { - Get-WinGetSource -Name 'TestSource' - } - catch { - # Source Remove requires admin privileges, this will only execute successfully in an elevated PowerShell. - # This is a workaround to an issue where the server takes longer than expected to terminate when - # running from PowerShell. This can cause other E2E tests to fail when attempting to reset the test source. - Start-Process -FilePath $wingetExeName -ArgumentList "source remove TestSource" + try { + # Source Remove requires admin privileges, this will only execute successfully in an elevated PowerShell. + $testSource = Get-WinGetSource | Where-Object -Property 'Name' -eq 'TestSource' + if ($null -ne $testSource) + { + # Source Remove requires admin privileges + Remove-WinGetSource -Name 'TestSource' } } + catch { + # Non-admin + Start-Process -FilePath $wingetExeName -ArgumentList "source remove TestSource" + } } function CreatePolicyKeyIfNotExists() @@ -125,7 +128,7 @@ Describe 'Get-WinGetVersion' { } } -Describe 'Get|Add|Reset-WinGetSource' -Skip:($PSEdition -eq "Desktop") { +Describe 'Get|Add|Reset-WinGetSource' { BeforeAll { AddTestSource @@ -150,7 +153,7 @@ Describe 'Get|Add|Reset-WinGetSource' -Skip:($PSEdition -eq "Desktop") { } } -Describe 'Find-WinGetPackage' -Skip:($PSEdition -eq "Desktop") { +Describe 'Find-WinGetPackage' { BeforeAll { AddTestSource @@ -203,7 +206,7 @@ Describe 'Find-WinGetPackage' -Skip:($PSEdition -eq "Desktop") { } } -Describe 'Install|Update|Uninstall-WinGetPackage' -Skip:($PSEdition -eq "Desktop") { +Describe 'Install|Update|Uninstall-WinGetPackage' { BeforeAll { AddTestSource @@ -279,7 +282,7 @@ Describe 'Install|Update|Uninstall-WinGetPackage' -Skip:($PSEdition -eq "Desktop } } -Describe 'Get-WinGetPackage' -Skip:($PSEdition -eq "Desktop") { +Describe 'Get-WinGetPackage' { BeforeAll { AddTestSource @@ -661,18 +664,6 @@ Describe 'WindowsPackageManagerServer' -Skip:($PSEdition -eq "Desktop") { } } -Describe 'WindowsPowerShell not supported' -Skip:($PSEdition -eq "Core") { - - It 'Throw not supported' { - { Find-WinGetPackage -Id "Fake.Id" } | Should -Throw "This cmdlet is not supported in Windows PowerShell." - { Get-WinGetPackage -Id "Fake.Id" } | Should -Throw "This cmdlet is not supported in Windows PowerShell." - { Install-WinGetPackage -Id "Fake.Id" } | Should -Throw "This cmdlet is not supported in Windows PowerShell." - { Uninstall-WinGetPackage -Id "Fake.Id" } | Should -Throw "This cmdlet is not supported in Windows PowerShell." - { Update-WinGetPackage -Id "Fake.Id" } | Should -Throw "This cmdlet is not supported in Windows PowerShell." - { Get-WinGetSource } | Should -Throw "This cmdlet is not supported in Windows PowerShell." - } -} - AfterAll { RemoveTestSource } \ No newline at end of file