From 8bcef7a657e24fca1faa3366935e360db43b5a28 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 31 Oct 2025 11:37:48 -0700 Subject: [PATCH] Make Repair-WGPM a COM-aware cmdlet and rework version retrieval (#5842) Fixes #5826 and #5827 ## Issue A previous change introduced a COM API to retrieve the WinGet version. The PowerShell methods to get the version were updated to try using it before invoking the existing method (run `winget --version`). This caused Repair-WGPM to use COM for the first time (IFF a version specifier was provided [which includes `-Latest`]). This caused the two linked issues: 1. #5826 :: In .NET Framework (Windows PowerShell), the .winmd file must be found in order to generate the COM type information at runtime. This is required when jit'ing the new version API, used only when a version specifier is provided. This doesn't affect .NET Core (PowerShell 7) because exceptions are swallowed to support backward compat and the types are all pre-generated by CsWinRT. Only commands deriving from a specific type were doing the initialization required. 2. #5827 :: Calling a COM API means that the server is active, making attempts to install the package fail due to an in-use error. This required `-Force` to be provided, again only if a version specifier was provided. ## Change The larger part of this change reworks the existing assert and repair state machine to better re-use the call to `winget --version` that is actually attempting to probe for WinGet CLI functionality. We keep that result around and use it when comparing to the supplied target version rather than attempting to retrieve the version again. If the version is not correct, we attach it to the exception that is thrown so that we can re-use it once again during the attempt to install the different version. Since the first attempt to call `winget --version` has to succeed in order to get to the code that would check the current version, we can successfully avoid the COM call in this path every time. Ultimately this means that if WinGet is already installed properly, attempting to change the version with Repair only gets the version once instead of the previous 3 times. The final portion of the change updates the base command for Repair and Assert to the one that provides the COM initialization. While this shouldn't be necessary with the other portion, it is preferable to supply `-Force` as a workaround than to simply wait for a resolution if the type cannot be loaded. --- .../Commands/WinGetPackageManagerCommand.cs | 12 ++++--- .../Common/WinGetIntegrity.cs | 12 ++++--- .../Exceptions/WinGetIntegrityException.cs | 8 ++++- .../Helpers/WinGetVersion.cs | 32 ++++++++++++++----- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index cdf5f16d51..b08e70ce60 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -21,7 +21,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// /// Used by Repair-WinGetPackageManager and Assert-WinGetPackageManager. /// - public sealed class WinGetPackageManagerCommand : BaseCommand + public sealed class WinGetPackageManagerCommand : ManagementDeploymentCommand { private const string EnvPath = "env:PATH"; @@ -132,7 +132,7 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers switch (currentCategory) { case IntegrityCategory.UnexpectedVersion: - await this.InstallDifferentVersionAsync(new WinGetVersion(expectedVersion), allUsers, force); + await this.InstallDifferentVersionAsync(new WinGetVersion(expectedVersion), e.InstalledVersion, allUsers, force); break; case IntegrityCategory.NotInPath: this.RepairEnvPath(); @@ -167,9 +167,13 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers } } - private async Task InstallDifferentVersionAsync(WinGetVersion toInstallVersion, bool allUsers, bool force) + private async Task InstallDifferentVersionAsync(WinGetVersion toInstallVersion, WinGetVersion? installedVersion, bool allUsers, bool force) { - var installedVersion = WinGetVersion.InstalledWinGetVersion(this); + if (installedVersion == null) + { + installedVersion = WinGetVersion.InstalledWinGetVersion(this); + } + bool isDowngrade = installedVersion.CompareAsDeployment(toInstallVersion) > 0; string message = $"Installed WinGet version '{installedVersion.TagVersion}' " + diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs index 9669da8dbc..d9ff12bb56 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -39,13 +39,14 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers return; } + WinGetCLICommandResult? versionResult = null; + try { // Start by calling winget without its WindowsApp PFN path. // If it succeeds and the exit code is 0 then we are good. - var wingetCliWrapper = new WingetCLIWrapper(false); - var result = wingetCliWrapper.RunCommand(pwshCmdlet, "--version"); - result.VerifyExitCode(); + versionResult = WinGetVersion.RunWinGetVersionFromCLI(pwshCmdlet, false); + versionResult.VerifyExitCode(); } catch (Win32Exception e) { @@ -68,7 +69,7 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers { // This assumes caller knows that the version exist. WinGetVersion expectedWinGetVersion = new WinGetVersion(expectedVersion); - var installedVersion = WinGetVersion.InstalledWinGetVersion(pwshCmdlet); + var installedVersion = WinGetVersion.InstalledWinGetVersion(pwshCmdlet, versionResult); if (expectedWinGetVersion.CompareTo(installedVersion) != 0) { throw new WinGetIntegrityException( @@ -76,7 +77,8 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers string.Format( Resources.IntegrityUnexpectedVersionMessage, installedVersion.TagVersion, - expectedVersion)); + expectedVersion)) + { InstalledVersion = installedVersion }; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs index 63a7939038..4ffee8283d 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -9,6 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions using System; using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Client.Engine.Helpers; using Microsoft.WinGet.Resources; /// @@ -53,6 +54,11 @@ public WinGetIntegrityException(IntegrityCategory category, string message) /// public IntegrityCategory Category { get; } + /// + /// Gets or sets the installed version. + /// + internal WinGetVersion? InstalledVersion { get; set; } + private static string GetMessage(IntegrityCategory category) => category switch { IntegrityCategory.Failure => Resources.IntegrityFailureMessage, diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs index 19d4704dd5..1961df90f1 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs @@ -64,23 +64,39 @@ public WinGetVersion(string version) /// public bool IsPrerelease { get; } + /// + /// Runs the winget version command. + /// + /// PowerShell cmdlet. + /// Use full path or not. + /// The command result. + public static WinGetCLICommandResult RunWinGetVersionFromCLI(PowerShellCmdlet pwshCmdlet, bool fullPath = true) + { + var wingetCliWrapper = new WingetCLIWrapper(fullPath); + return wingetCliWrapper.RunCommand(pwshCmdlet, "--version"); + } + /// /// Gets the version of the installed winget. /// /// PowerShell cmdlet. + /// A command result from running previously. /// The WinGetVersion. - public static WinGetVersion InstalledWinGetVersion(PowerShellCmdlet pwshCmdlet) + public static WinGetVersion InstalledWinGetVersion(PowerShellCmdlet pwshCmdlet, WinGetCLICommandResult? versionResult = null) { - // Try getting the version through COM if it is available (user might have an older build installed) - string? comVersion = PackageManagerWrapper.Instance.GetVersion(); - if (comVersion != null) + if (versionResult == null || versionResult.ExitCode != 0) { - return new WinGetVersion(comVersion); + // Try getting the version through COM if it is available (user might have an older build installed) + string? comVersion = PackageManagerWrapper.Instance.GetVersion(); + if (comVersion != null) + { + return new WinGetVersion(comVersion); + } + + versionResult = RunWinGetVersionFromCLI(pwshCmdlet); } - var wingetCliWrapper = new WingetCLIWrapper(); - var result = wingetCliWrapper.RunCommand(pwshCmdlet, "--version"); - return new WinGetVersion(result.StdOut.Replace(Environment.NewLine, string.Empty)); + return new WinGetVersion(versionResult.StdOut.Replace(Environment.NewLine, string.Empty)); } ///