winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 84d3e87847edcd9faa3985f6b84a099d95c913bd
parent 937ec874ecb707eb2df37b74ebe0100ccb7e9261
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Thu, 27 Apr 2023 12:41:45 -0700

Start Microsoft.WinGet.Configuration module (#3187)

This PR sets the foundation for the new PowerShell module Microsoft.WinGet.Configuration which is going to be an equivalent of winget configure

The module is going to be built as a binary module and it supports using its own ACL to load dependencies. The Microsoft.WinGet.Configuration.Cmdlets project will contain all the cmdlet entry points and Microsoft.WinGet.Configuration.Engine the actual logic.

This module consumes Microsoft.Management.Configuration which uses cswinrt and requires being run on an MTA. This is achieved via MtaCommand.cs. This class contains functions that will verify if the current thread is already running on an MTA (via pwsh.exe -MTA) and if not start a new MTA thread. Every command that consumes winrt objects must implement it.

MtaCommand.cs also implements methods to handle waiting for asynchronous calls and allows sending messages to the PowerShell host that are required to be executed in the main thread (only WriteDebug is implemented for now, but any other method follows the same approach). This is based on the async mechanism that azure-powershell uses.
Diffstat:
M.github/actions/spelling/expect.txt | 3+++
Msrc/AppInstallerCLI.sln | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj | 2+-
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Acl/CustomAssemblyLoadContext.cs | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Acl/ModuleInit.cs | 29+++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Utilities.cs | 35+++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Microsoft.WinGet.Configuration.Cmdlets.csproj | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/MtaCommand.cs | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 | 33+++++++++++++++++++++++++++++++++
Msrc/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 | 9+++++++--
13 files changed, 798 insertions(+), 3 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -67,6 +67,7 @@ CODEOWNERS COINIT COMGLB commandline +Configx contactsupport contentfiles contoso @@ -229,6 +230,7 @@ msftrubengu MSIHASH MSIXHASH msstore +Mta Mugiwara Multideclaration mycustom @@ -371,6 +373,7 @@ sysrefcomp systemnotsupported Tagit TCpp +tcs Templating temppath testexampleinstaller diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -163,6 +163,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationRemotingServer EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.WinGet.Client.Engine", "PowerShell\Microsoft.WinGet.Client.Engine\Microsoft.WinGet.Client.Engine.csproj", "{1F56BECB-D65D-4BBA-8788-6671B251392A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.WinGet.Configuration.Cmdlets", "PowerShell\Microsoft.WinGet.Configuration.Cmdlets\Microsoft.WinGet.Configuration.Cmdlets.csproj", "{167F634B-A3AD-494E-8E67-B888103E35FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.WinGet.Configuration.Engine", "PowerShell\Microsoft.WinGet.Configuration.Engine\Microsoft.WinGet.Configuration.Engine.csproj", "{C54F80ED-B736-49B0-9BD3-662F57024D01}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -900,6 +904,54 @@ Global {1F56BECB-D65D-4BBA-8788-6671B251392A}.TestRelease|x64.Build.0 = Release|x64 {1F56BECB-D65D-4BBA-8788-6671B251392A}.TestRelease|x86.ActiveCfg = Release|x86 {1F56BECB-D65D-4BBA-8788-6671B251392A}.TestRelease|x86.Build.0 = Release|x86 + {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|ARM64.Build.0 = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|x64.ActiveCfg = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|x64.Build.0 = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Debug|x86.Build.0 = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|ARM64.Build.0 = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|x64.ActiveCfg = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|x64.Build.0 = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|x86.ActiveCfg = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Fuzzing|x86.Build.0 = Debug|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Release|ARM64.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Release|ARM64.Build.0 = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Release|x64.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Release|x64.Build.0 = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Release|x86.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.Release|x86.Build.0 = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.TestRelease|ARM64.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.TestRelease|ARM64.Build.0 = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.TestRelease|x64.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.TestRelease|x64.Build.0 = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.TestRelease|x86.ActiveCfg = Release|Any CPU + {167F634B-A3AD-494E-8E67-B888103E35FF}.TestRelease|x86.Build.0 = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|ARM64.Build.0 = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|x64.ActiveCfg = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|x64.Build.0 = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|x86.ActiveCfg = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Debug|x86.Build.0 = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|ARM64.Build.0 = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|x64.ActiveCfg = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|x64.Build.0 = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|x86.ActiveCfg = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Fuzzing|x86.Build.0 = Debug|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Release|ARM64.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Release|ARM64.Build.0 = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Release|x64.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Release|x64.Build.0 = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Release|x86.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.Release|x86.Build.0 = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.TestRelease|ARM64.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.TestRelease|ARM64.Build.0 = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.TestRelease|x64.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.TestRelease|x64.Build.0 = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.TestRelease|x86.ActiveCfg = Release|Any CPU + {C54F80ED-B736-49B0-9BD3-662F57024D01}.TestRelease|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -926,6 +978,8 @@ Global {8E43F982-40D5-4DF1-9044-C08047B5F43B} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {BB14D603-F44E-4415-8770-BF3E13F4C17F} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {1F56BECB-D65D-4BBA-8788-6671B251392A} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} + {167F634B-A3AD-494E-8E67-B888103E35FF} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} + {C54F80ED-B736-49B0-9BD3-662F57024D01} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} diff --git a/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj b/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj @@ -15,7 +15,7 @@ <NoWarn>1591</NoWarn> </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)'=='Debug'"> + <PropertyGroup Condition="'$(Configuration)'=='Release'"> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Acl/CustomAssemblyLoadContext.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Acl/CustomAssemblyLoadContext.cs @@ -0,0 +1,112 @@ +// ----------------------------------------------------------------------------- +// <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.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 + { + 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<string> ValidArchs = new string[] { "x86", "x64", "arm64" }; + + private static readonly CustomAssemblyLoadContext WinGetAcl = new (); + + private readonly string sharedArchDependencyPath; + + private CustomAssemblyLoadContext() + : base("WinGetAssemblyLoadContext", isCollectible: false) + { + var arch = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE", EnvironmentVariableTarget.Process); + if (string.IsNullOrEmpty(arch)) + { + throw new ArgumentNullException("PROCESSOR_ARCHITECTURE"); + } + + arch = arch.ToLower(); + if (arch == "amd64") + { + arch = "x64"; + } + + if (!ValidArchs.Contains(arch)) + { + throw new NotSupportedException(arch); + } + + this.sharedArchDependencyPath = Path.Combine(SharedDependencyPath, arch); + } + + /// <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 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 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 @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------------- +// <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/Cmdlets/InvokeWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs @@ -0,0 +1,50 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InvokeWinGetConfigurationCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using System.Threading; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Helpers; + + /// <summary> + /// Invoke-WinGetConfiguration. + /// Start configuration and waits for completion. + /// </summary> + [Cmdlet(VerbsLifecycle.Invoke, "WinGetConfiguration")] + public sealed class InvokeWinGetConfigurationCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the configuration file. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipelineByPropertyName = true)] + public string File { get; set; } + + /// <summary> + /// Pre-processing operations. + /// </summary> + protected override void BeginProcessing() + { + // The cmdlet doesn't inherit the location from the current session. + // Change it to support relative paths. + Utilities.ChangeToCurrentSessionLocation(); + } + + /// <summary> + /// Starts configuration and wait for it to complete. + /// </summary> + protected override void ProcessRecord() + { + CancellationTokenSource source = new (); + var configCommand = new ConfigurationCommand(this, source.Token, this.File); + + configCommand.Invoke(); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Utilities.cs @@ -0,0 +1,35 @@ +// ----------------------------------------------------------------------------- +// <copyright file="Utilities.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Helpers +{ + using System; + using System.IO; + using System.Linq; + using System.Management.Automation; + + /// <summary> + /// Utilities for this cmdlets. + /// </summary> + internal static class Utilities + { + /// <summary> + /// Change the current session to the location where the cmdlet + /// got executed. + /// </summary> + public static void ChangeToCurrentSessionLocation() + { + var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); + var results = ps.AddCommand("Get-Location").Invoke<PathInfo>(); + if (results is null || results.Count == 0) + { + throw new InvalidOperationException(); + } + + Directory.SetCurrentDirectory(results.First().Path); + } + } +} 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 @@ -0,0 +1,125 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0-windows10.0.19041</TargetFramework> + <OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutputPath> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <DocumentationFile>$(OutputPath)\$(MSBuildProjectName).xml</DocumentationFile> + <RuntimeIdentifier>win</RuntimeIdentifier> + <BuildOutputDirectory>$(SolutionDir)$(Platform)\$(Configuration)\</BuildOutputDirectory> + <RootNamespace>Microsoft.WinGet.Configuration</RootNamespace> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)'=='Release'"> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + </PropertyGroup> + + <ItemGroup> + <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <!-- This project doesn't reference CSWinRT, but specify this everywhere it will fail with NETSDK1130 and MSB3271. --> + <PropertyGroup> + <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Microsoft.WinGet.Configuration.Engine\Microsoft.WinGet.Configuration.Engine.csproj" /> + </ItemGroup> + + <!-- Build PowerShell Module --> + <PropertyGroup> + <PowerShellModuleOutputDirectory>$(BuildOutputDirectory)PowerShell\Microsoft.WinGet.Configuration</PowerShellModuleOutputDirectory> + <PowerShellModuleSharedDependencies>$(PowerShellModuleOutputDirectory)\SharedDependencies</PowerShellModuleSharedDependencies> + <PowerShellModuleDirectDependencies>$(PowerShellModuleOutputDirectory)\DirectDependencies</PowerShellModuleDirectDependencies> + </PropertyGroup> + + <!-- PowerShell Files--> + <Target Name="CopyModuleFiles" AfterTargets="AfterBuild"> + <ItemGroup> + <ModuleFiles Include="$(ProjectDir)\..\Microsoft.WinGet.Configuration\ModuleFiles\**\*.*" /> + </ItemGroup> + <Message Importance="high" Text="Copying PowerShell module files to '$(PowerShellModuleOutputDirectory)'" /> + <Copy SourceFiles="@(ModuleFiles)" DestinationFiles="$(PowerShellModuleOutputDirectory)\%(RecursiveDir)%(Filename)%(Extension)" /> + </Target> + + <!-- Main binaries --> + <Target Name="CopyCmdletBinaries" AfterTargets="AfterBuild" Condition="'$(Platform)' != 'ARM'"> + <ItemGroup> + <ModuleCmdletFile Include="$(OutputPath)Microsoft.WinGet.Configuration.Cmdlets.dll" /> + <ModuleCmdletFile Include="$(OutputPath)Microsoft.WinGet.Configuration.Cmdlets.pdb" /> + </ItemGroup> + <Message Importance="high" Text="Copying CopyCmdletBinaries: '@(ModuleCmdletFile)'" /> + <Copy SourceFiles="@(ModuleCmdletFile)" DestinationFolder="$(PowerShellModuleOutputDirectory)" /> + </Target> + + <!-- Direct dependencies --> + <Target Name="CopyDirectDependencies" AfterTargets="AfterBuild" Condition="'$(Platform)' != 'ARM'"> + <ItemGroup> + <ModuleDirectDependency Include="$(OutputPath)Microsoft.WinGet.Configuration.Engine.dll" /> + <ModuleDirectDependency Include="$(OutputPath)Microsoft.WinGet.Configuration.Engine.pdb" /> + </ItemGroup> + <Message Importance="high" Text="Copying direct dependencies: '@(ModuleDirectDependency)'" /> + <Copy SourceFiles="@(ModuleDirectDependency)" DestinationFolder="$(PowerShellModuleDirectDependencies)" /> + </Target> + + <!-- Shared dependencies --> + <Target Name="CopySharedDependencies" AfterTargets="AfterBuild" Condition="'$(Platform)' != 'ARM'"> + <ItemGroup> + <ModuleSharedDependency Include="$(OutputPath)Microsoft.Management.Configuration.Processor.dll" /> + <ModuleSharedDependency Include="$(OutputPath)Microsoft.Windows.SDK.NET.dll" /> + <ModuleSharedDependency Include="$(OutputPath)WinRT.Runtime.dll" /> + </ItemGroup> + <Message Importance="high" Text="Copying shared dependencies: '@(ModuleSharedDependency)'" /> + <Copy SourceFiles="@(ModuleSharedDependency)" DestinationFolder="$(PowerShellModuleSharedDependencies)" /> + </Target> + + <!-- Since this is Any CPU we will have to probe for arch specific binaries --> + <!-- This gets defined before the target framework and the rid gets appended to the OutputPath --> + <PropertyGroup> + <Configx64Path>$(OutputPath)..\..\..\x64\$(Configuration)\Microsoft.Management.Configuration\</Configx64Path> + <Configx86Path>$(OutputPath)..\..\..\x86\$(Configuration)\Microsoft.Management.Configuration\</Configx86Path> + <ConfigArm64Path>$(OutputPath)..\..\..\ARM64\$(Configuration)\Microsoft.Management.Configuration\</ConfigArm64Path> + </PropertyGroup> + + <!-- The projection dll is Any CPU, but WinRT.Runtime.dll expects file to be in the same location as the native dll --> + <Target Name="CopyConfigx64" AfterTargets="AfterBuild" Condition="Exists('$(Configx64Path)Microsoft.Management.Configuration.dll')"> + <ItemGroup> + <Configx64File Include="$(OutputPath)\Microsoft.Management.Configuration.Projection.dll" /> + <Configx64File Include="$(Configx64Path)\Microsoft.Management.Configuration.dll" /> + </ItemGroup> + <Message Importance="high" Text="Copying x64 '@(Configx64File)'" /> + <Copy SourceFiles="@(Configx64File)" DestinationFolder="$(PowerShellModuleSharedDependencies)\x64" /> + </Target> + + <Target Name="CopyConfigx86" AfterTargets="AfterBuild" Condition="Exists('$(Configx86Path)Microsoft.Management.Configuration.dll')"> + <ItemGroup> + <Configx86File Include="$(OutputPath)\Microsoft.Management.Configuration.Projection.dll" /> + <Configx86File Include="$(Configx86Path)\Microsoft.Management.Configuration.dll" /> + </ItemGroup> + <Message Importance="high" Text="Copying x86 '@(Configx86File)'" /> + <Copy SourceFiles="@(Configx86File)" DestinationFolder="$(PowerShellModuleSharedDependencies)\x86" /> + </Target> + + <Target Name="CopyConfigArm64" AfterTargets="AfterBuild" Condition="Exists('$(ConfigArm64Path)Microsoft.Management.Configuration.dll')"> + <ItemGroup> + <ConfigArm64File Include="$(OutputPath)\Microsoft.Management.Configuration.Projection.dll" /> + <ConfigArm64File Include="$(ConfigArm64Path)\Microsoft.Management.Configuration.dll" /> + </ItemGroup> + <Message Importance="high" Text="Copying arm64 '@(ConfigArm64File)'" /> + <Copy SourceFiles="@(ConfigArm64File)" DestinationFolder="$(PowerShellModuleSharedDependencies)\arm64" /> + </Target> + +</Project> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs @@ -0,0 +1,86 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Commands +{ + using System; + using System.IO; + using System.Management.Automation; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor; + using Windows.Storage; + using Windows.Storage.Streams; + + /// <summary> + /// Class that deals with start and invoke the configuration. + /// </summary> + public sealed class ConfigurationCommand : MtaCommand + { + private readonly string configFile; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationCommand"/> class. + /// </summary> + /// <param name="psCmdlet">PSCmdlet.</param> + /// <param name="cancellationToken">Cancellation token.</param> + /// <param name="configFile">Configuration file path.</param> + public ConfigurationCommand(PSCmdlet psCmdlet, CancellationToken cancellationToken, string configFile) + : base(psCmdlet, cancellationToken) + { + if (!File.Exists(configFile)) + { + throw new FileNotFoundException(configFile); + } + + this.configFile = configFile; + } + + /// <summary> + /// Invoke configuration. Waits until completed. + /// </summary> + public void Invoke() + { + // Start task. + var runningTask = this.RunOnMTA(this.InvokeAsync); + + // Wait for it to complete or being cancelled. + this.Wait(runningTask); + } + + private async Task InvokeAsync() + { + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.MTA) + { + throw new NotSupportedException("Calling from an STA"); + } + + // This will fail with E_NOTIMPL + var factory = new ConfigurationSetProcessorFactory( + ConfigurationProcessorType.Default, null); + var processor = new ConfigurationProcessor(factory); + + var configSet = await this.CreateConfigurationSetAsync(processor); + _ = await processor.GetSetDetailsAsync(configSet, ConfigurationUnitDetailLevel.Catalog); + } + + private async Task<ConfigurationSet> CreateConfigurationSetAsync(ConfigurationProcessor processor) + { + var stream = await FileRandomAccessStream.OpenAsync(this.configFile, FileAccessMode.Read); + + OpenConfigurationSetResult openResult = await processor.OpenConfigurationSetAsync(stream); + + if (openResult.Set is null) + { + // TODO: throw better exception. + throw new Exception($"Failed opening configuration set. Result 0x{openResult.ResultCode} at {openResult.Field}"); + } + + return openResult.Set; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/MtaCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/MtaCommand.cs @@ -0,0 +1,222 @@ +// ----------------------------------------------------------------------------- +// <copyright file="MtaCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Commands +{ + using System; + using System.Management.Automation; + using System.Threading; + using System.Threading.Tasks; + + /// <summary> + /// This is the base class for any command that needs to be executed on an MTA thread. + /// Call RunOnMTA to start any async function call. If the thread is already running + /// on an MTA it will executed it, otherwise it will create a new MTA thread. + /// + /// Calling PSCmdlet functions to write into their stream from not the main thread will + /// throw an exception. + /// This class contains wrappers around those methods with synchronization mechanisms + /// to output the messages. + /// </summary> + public abstract class MtaCommand + { + private readonly Thread originalThread; + + private readonly SemaphoreSlim semaphore = new (1, 1); + private readonly ManualResetEventSlim mainThreadActionReady = new (false); + private readonly ManualResetEventSlim mainThreadActionCompleted = new (false); + private readonly CancellationToken cancellationToken; + + private Action? mainThreadAction = null; + + /// <summary> + /// Initializes a new instance of the <see cref="MtaCommand"/> class. + /// </summary> + /// <param name="psCmdlet">PSCmdlet.</param> + /// <param name="cancellationToken">Cancellation token.</param> + public MtaCommand(PSCmdlet psCmdlet, CancellationToken cancellationToken) + { + this.PsCmdlet = psCmdlet; + this.originalThread = Thread.CurrentThread; + this.cancellationToken = cancellationToken; + } + + /// <summary> + /// Gets the base cmdlet. + /// </summary> + protected PSCmdlet PsCmdlet { get; private set; } + + /// <summary> + /// Execute the delegate in a MTA thread. + /// </summary> + /// <param name="func">Function to execute.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + public Task RunOnMTA(Func<Task> func) + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) + { + this.WriteDebug("Already running on MTA"); + return func(); + } + + this.WriteDebug("Creating MTA thread"); + var tcs = new TaskCompletionSource(); + var thread = new Thread(() => + { + try + { + func().GetAwaiter().GetResult(); + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + + thread.SetApartmentState(ApartmentState.MTA); + thread.Start(); + return tcs.Task; + } + + /// <summary> + /// Execute the delegate in a MTA thread. + /// </summary> + /// <param name="func">Function to execute.</param> + /// <typeparam name="TResult">Return type of function.</typeparam> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + public Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func) + where TResult : struct + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) + { + this.WriteDebug("Already running on MTA"); + return func(); + } + + this.WriteDebug("Creating MTA thread"); + var tcs = new TaskCompletionSource<TResult>(); + var thread = new Thread(() => + { + try + { + var result = func().GetAwaiter().GetResult(); + tcs.SetResult(result); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + + thread.SetApartmentState(ApartmentState.MTA); + thread.Start(); + return tcs.Task; + } + + /// <summary> + /// Waits for the task to be completed. This MUST be called from the main thread. + /// </summary> + /// <param name="runningTask">Task to wait for.</param> + public void Wait(Task runningTask) + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + do + { + // Wait for the running task to be completed or if there's + // an action that needs to be executed in the main thread. + WaitHandle.WaitAny(new[] + { + this.mainThreadActionReady.WaitHandle, + ((IAsyncResult)runningTask).AsyncWaitHandle, + }); + + if (this.mainThreadActionReady.IsSet) + { + // Someone needs the main thread. + this.mainThreadActionReady.Reset(); + + if (this.mainThreadAction != null) + { + this.mainThreadAction(); + } + + // Done. + this.mainThreadActionCompleted.Set(); + } + } + while (!runningTask.IsCompleted); + + if (runningTask.IsFaulted) + { + // If IsFaulted is true, the task's Status is equal to Faulted, + // and its Exception property will be non-null. + throw runningTask.Exception!; + } + } + + /// <summary> + /// Calls cmdlet WriteDebug. + /// If its executed on the main thread calls it directly. Otherwise + /// sets it to the main thread action and wait for it to be executed. + /// </summary> + /// <param name="text">Debug text.</param> + protected void WriteDebug(string text) + { + if (this.originalThread == Thread.CurrentThread) + { + this.PsCmdlet.WriteDebug(text); + return; + } + + try + { + this.WaitForOurTurn(); + this.mainThreadAction = () => this.PsCmdlet.WriteDebug(text); + this.mainThreadActionReady.Set(); + this.WaitMainThreadActionCompletion(); + } + catch (Exception) + { + throw; + } + } + + private void WaitForOurTurn() + { + this.semaphore.Wait(this.cancellationToken); + this.mainThreadActionCompleted.Reset(); + } + + private void WaitMainThreadActionCompletion() + { + WaitHandle.WaitAny(new[] + { + this.cancellationToken.WaitHandle, + this.mainThreadActionCompleted.WaitHandle, + }); + + this.semaphore.Release(); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj @@ -0,0 +1,41 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0-windows10.0.19041</TargetFramework> + <Nullable>enable</Nullable> + <OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutputPath> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <DocumentationFile>$(OutputPath)\$(MSBuildProjectName).xml</DocumentationFile> + <RuntimeIdentifier>win</RuntimeIdentifier> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)'=='Release'"> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + </PropertyGroup> + + <ItemGroup> + <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <!-- This project doesn't reference CSWinRT, but specify this everywhere it will fail with NETSDK1130 and MSB3271. --> + <PropertyGroup> + <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Microsoft.Management.Configuration.Processor\Microsoft.Management.Configuration.Processor.csproj" /> + </ItemGroup> + +</Project> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 b/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 @@ -0,0 +1,33 @@ +@{ + +RootModule = "Microsoft.WinGet.Configuration.Cmdlets.dll" +ModuleVersion = '0.0.1' +CompatiblePSEditions = 'Core' +GUID = '79b6b07b-7be5-4673-9cd1-fcbe3d79ba82' +Author = 'Microsoft Corporation' +CompanyName = 'Microsoft Corporation' +Copyright = '(c) Microsoft Corporation. All rights reserved.' +Description = 'PowerShell Module for the Windows Package Manager Configuration.' +PowerShellVersion = '7.2.8' + +# FormatsToProcess = @() +FunctionsToExport = @() +AliasesToExport = @() + +CmdletsToExport = @( + "Invoke-WinGetConfiguration" +) + +PrivateData = @{ + PSData = @{ + Tags = @( + 'WindowsPackageManager', + 'WinGet' + ) + ProjectUri = 'https://github.com/microsoft/winget-cli' + Prerelease = 'alpha' + } +} + +} + diff --git a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -20,7 +20,7 @@ [CmdletBinding()] param ( [Parameter(Mandatory)] - [ValidateSet("x64", "x86")] + [ValidateSet("x64", "x86", "AnyCpu")] [string] $Platform, @@ -64,7 +64,12 @@ $local:moduleRootOutput = "$PSScriptRoot\Module\" "Microsoft.WinGet.Client", "$PSScriptRoot\..\Microsoft.WinGet.Client\ModuleFiles\", $true, - $true) + $true), + [WinGetModule]::new( + "Microsoft.WinGet.Configuration", + "$PSScriptRoot\..\Microsoft.WinGet.Configuration\ModuleFiles\", + $true, + $false) foreach($module in $modules) {