commit 27f90003f72825b27ccf1bbff07aaa6a47c469dd parent 6ea13623e5e4b870b81efeea9142d15a98dd4208 Author: Madhusudhan-MSFT <53235553+Madhusudhan-MSFT@users.noreply.github.com> Date: Sun, 6 Oct 2024 21:08:16 -0700 WinGet COM API for Repair (#4736) This PR introduces the COM API for WinGet Repair Operation. Key updates include: - PackageManager.IDL updates for Repair API contracts. - Implementation of Repair COM API methods in PackageManager runtime class. - Addition of runtime classes for RepairOptions and RepairResults. - Integration logic connecting COM API with existing Repair Workflow. - Updates to WinGetCLIPackage manifest for Repair COM API GUIDs. - Changes to Microsoft.Management.Deployment.InProc to support E2E testing of Repair COM API interops. Add Repair-WinGetPackage powershell cmdlet to Microsoft.WinGet.Client - Introduced a new cmdlet `Repair-WinGetPackage` to the Microsoft.WinGet.Client PowerShell module **[Tests:]** Added E2E tests for Winget COM Repair API: - Introduced RepairInterop.cs for InProc & Out of Proc COM Repair APIs. - Extended GroupPolicyForInterop.cs to cover COM Repair API scenarios. Added PowerShell pester tests for Repair-WinGetPackage cmdlet. **[Validation:]** - Executed RepairInterop E2E tests and confirmed all tests passed. - Executed GroupPolicyForInterop E2E tests and confirmed all tests passed. - Executed RepairCommand E2E tests and confirmed all tests passed.  - Executed the Repair-WinGetPackage PowerShell cmdlet manually for a set of test packages and confirmed that it returns the expected results. ex: ``` - AppInstallerTest.TestMsixInstaller - Install-WinGetPackage -Id AppInstallerTest.TestMsixInstaller - return status OK - Repair-WinGetPackage -Id AppInstallerTest.TestMsixInstaller - return status OK - Uninstall-WinGetPackage -Id AppInstallerTest.TestMsixInstaller - return status OK - AppInstallerTest.TestModifyRepair - Install-WinGetPackage -Id AppInstallerTest.TestModifyRepair - return status OK - Repair-WinGetPackage -Id AppInstallerTest.TestModifyRepair - return status OK - Uninstall-WinGetPackage -Id AppInstallerTest.TestModifyRepair - return status OK ``` <!-- To check a checkbox place an "x" between the brackets. e.g: [x] --> - [x] I have signed the [Contributor License Agreement](https://cla.opensource.microsoft.com/microsoft/winget-pkgs). - [x] This pull request is related to an issue. ----- ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4736) Diffstat:
54 files changed, 2334 insertions(+), 98 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -185,6 +185,7 @@ fundraiser fuzzer fzanollo gcpi +GDK GES GESMBH getwinget diff --git a/src/AppInstallerCLICore/Commands/COMCommand.cpp b/src/AppInstallerCLICore/Commands/COMCommand.cpp @@ -7,7 +7,8 @@ #include "Workflows/PromptFlow.h" #include "Workflows/UninstallFlow.h" #include "Workflows/WorkflowBase.h" -#include "Workflows/DependenciesFlow.h" +#include "Workflows/DependenciesFlow.h" +#include "Workflows/RepairFlow.h" namespace AppInstaller::CLI { @@ -45,4 +46,12 @@ namespace AppInstaller::CLI context << Workflow::UninstallSinglePackage; } + + // IMPORTANT: To use this command, the caller should have already retrieved the InstalledPackageVersion and added it to the Context Data + void COMRepairCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::SelectApplicableInstallerIfNecessary << + Workflow::RepairSinglePackage; + } } diff --git a/src/AppInstallerCLICore/Commands/COMCommand.h b/src/AppInstallerCLICore/Commands/COMCommand.h @@ -33,5 +33,15 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; + }; + + // IMPORTANT: To use this command, the caller should have already retrieved the InstalledPackageVersion and added it to the Context Data + struct COMRepairCommand final : public Command + { + constexpr static std::string_view CommandName = "repair"sv; + COMRepairCommand(std::string_view parent) : Command(CommandName, parent) {} + + protected: + void ExecuteInternal(Execution::Context& context) const override; }; } diff --git a/src/AppInstallerCLICore/ContextOrchestrator.cpp b/src/AppInstallerCLICore/ContextOrchestrator.cpp @@ -12,7 +12,7 @@ namespace AppInstaller::CLI::Execution { namespace { - // Operation command queue used by install and uninstall commands. + // Operation command queue used by install, uninstall and repair commands. constexpr static std::string_view OperationCommandQueueName = "operation"sv; // Callback function used by worker threads in the queue. @@ -30,7 +30,7 @@ namespace AppInstaller::CLI::Execution // Get command queue name based on command name. std::string_view GetCommandQueueName(std::string_view commandName) { - if (commandName == COMInstallCommand::CommandName || commandName == COMUninstallCommand::CommandName) + if (commandName == COMInstallCommand::CommandName || commandName == COMUninstallCommand::CommandName || commandName == COMRepairCommand::CommandName) { return OperationCommandQueueName; } @@ -363,6 +363,7 @@ namespace AppInstaller::CLI::Execution case PackageOperationType::Upgrade: return "root:upgrade"sv; case PackageOperationType::Uninstall: return "root:uninstall"sv; case PackageOperationType::Download: return "root:download"sv; + case PackageOperationType::Repair: return "root:repair"sv; default: return "unknown"; } } @@ -394,4 +395,11 @@ namespace AppInstaller::CLI::Execution item->AddCommand(std::make_unique<::AppInstaller::CLI::COMDownloadCommand>(RootCommand::CommandName)); return item; } + + std::unique_ptr<OrchestratorQueueItem> OrchestratorQueueItemFactory::CreateItemForRepair(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context) + { + std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), PackageOperationType::Repair); + item->AddCommand(std::make_unique<::AppInstaller::CLI::COMRepairCommand>(RootCommand::CommandName)); + return item; + } } diff --git a/src/AppInstallerCLICore/ContextOrchestrator.h b/src/AppInstallerCLICore/ContextOrchestrator.h @@ -47,6 +47,7 @@ namespace AppInstaller::CLI::Execution Upgrade, Uninstall, Download, + Repair, }; struct OrchestratorQueueItem @@ -101,6 +102,8 @@ namespace AppInstaller::CLI::Execution static std::unique_ptr<OrchestratorQueueItem> CreateItemForSearch(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context); // Create queue item for download static std::unique_ptr<OrchestratorQueueItem> CreateItemForDownload(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context); + // Create queue item for repair + static std::unique_ptr<OrchestratorQueueItem> CreateItemForRepair(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context); }; struct ContextOrchestrator diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -60,6 +60,7 @@ namespace AppInstallerCLIE2ETests public const string MsiInstallerV2FileName = "AppInstallerTestMsiInstallerV2.msi"; public const string MsixInstallerFileName = "AppInstallerTestMsixInstaller.msix"; public const string ZipInstallerFileName = "AppInstallerTestZipInstaller.zip"; + public const string ModifyRepairInstaller = "AppInstallerTest.TestModifyRepair"; public const string IndexPackage = "source.msix"; public const string MakeAppx = "makeappx.exe"; public const string SignTool = "signtool.exe"; diff --git a/src/AppInstallerCLIE2ETests/Interop/GroupPolicyForInterop.cs b/src/AppInstallerCLIE2ETests/Interop/GroupPolicyForInterop.cs @@ -8,6 +8,7 @@ namespace AppInstallerCLIE2ETests.Interop { using System; using System.IO; + using System.Text; using System.Threading.Tasks; using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; @@ -86,6 +87,10 @@ namespace AppInstallerCLIE2ETests.Interop Assert.AreEqual(Constants.BlockByWinGetPolicyErrorMessage, groupPolicyException.Message); Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, groupPolicyException.HResult); + groupPolicyException = Assert.Catch<GroupPolicyException>(() => { RepairOptions repairOptions = this.TestFactory.CreateRepairOptions(); }); + Assert.AreEqual(Constants.BlockByWinGetPolicyErrorMessage, groupPolicyException.Message); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, groupPolicyException.HResult); + // PackageManagerSettings is not implemented in context OutOfProcDev if (this.TestFactory.Context == ClsidContext.InProc) { @@ -116,33 +121,44 @@ namespace AppInstallerCLIE2ETests.Interop PackageCatalogReference compositeSource = packageManager.CreateCompositePackageCatalog(options); // Find package - var searchResult = this.FindOnePackage(compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, Constants.ExeInstallerPackageId); + var searchResult = this.FindOnePackage(compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, Constants.ModifyRepairInstaller); // Configure installation var installOptions = this.TestFactory.CreateInstallOptions(); - installOptions.PackageInstallMode = PackageInstallMode.Silent; - installOptions.PreferredInstallLocation = installDir; installOptions.AcceptPackageAgreements = true; + installOptions.ReplacementInstallerArguments = $"/InstallDir {installDir} /Version 2.0.0.0 /DisplayName TestModifyRepair /UseHKLM"; // Install var installResult = await packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); // Find package again, but this time it should detect the installed version - searchResult = this.FindOnePackage(compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, Constants.ExeInstallerPackageId); + searchResult = this.FindOnePackage(compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, Constants.ModifyRepairInstaller); Assert.NotNull(searchResult.CatalogPackage.InstalledVersion); + // Repair + var repairOptions = this.TestFactory.CreateRepairOptions(); + repairOptions.PackageRepairMode = PackageRepairMode.Silent; + var repairResult = await packageManager.RepairPackageAsync(searchResult.CatalogPackage, repairOptions); + Assert.AreEqual(RepairResultStatus.Ok, repairResult.Status); + // Uninstall var uninstallResult = await packageManager.UninstallPackageAsync(searchResult.CatalogPackage, this.TestFactory.CreateUninstallOptions()); Assert.AreEqual(UninstallResultStatus.Ok, uninstallResult.Status); Assert.True(TestCommon.VerifyTestExeUninstalled(installDir)); + // Clean up. + if (Directory.Exists(installDir)) + { + Directory.Delete(installDir, true); + } + // Download var downloadResult = await packageManager.DownloadPackageAsync(searchResult.CatalogPackage, this.TestFactory.CreateDownloadOptions()); Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); var packageVersion = "2.0.0.0"; - string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ExeInstallerPackageId}_{packageVersion}"); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ModifyRepairInstaller}_{packageVersion}"); + Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestModifyRepair", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Burn, "en-US")); } } } diff --git a/src/AppInstallerCLIE2ETests/Interop/RepairInterop.cs b/src/AppInstallerCLIE2ETests/Interop/RepairInterop.cs @@ -0,0 +1,356 @@ +// ----------------------------------------------------------------------------- +// <copyright file="RepairInterop.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests.Interop +{ + using System; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using AppInstallerCLIE2ETests.Helpers; + using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment.Projection; + using NUnit.Framework; + + /// <summary> + /// Repair Interop Tests for COM/WinRT Interop classes. + /// </summary> + [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.InProcess), Category = nameof(InstanceInitializersSource.InProcess))] + [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.OutOfProcess), Category = nameof(InstanceInitializersSource.OutOfProcess))] + public class RepairInterop : BaseInterop + { + private string installDir; + private PackageManager packageManager; + private PackageCatalogReference compositeSource; + + /// <summary> + /// Initializes a new instance of the <see cref="RepairInterop"/> class. + /// </summary> + /// <param name="initializer">Initializer.</param> + public RepairInterop(IInstanceInitializer initializer) + : base(initializer) + { + } + + /// <summary> + /// Test setup. + /// </summary> + [SetUp] + public void Init() + { + this.packageManager = this.TestFactory.CreatePackageManager(); + this.installDir = TestCommon.GetRandomTestDir(); + + // Create a composite source + var options = this.TestFactory.CreateCreateCompositePackageCatalogOptions(); + var testSource = this.packageManager.GetPackageCatalogByName(Constants.TestSourceName); + Assert.NotNull(testSource, $"{Constants.TestSourceName} cannot be null"); + options.Catalogs.Add(testSource); + options.CompositeSearchBehavior = CompositeSearchBehavior.AllCatalogs; + this.compositeSource = this.packageManager.CreateCompositePackageCatalog(options); + } + + /// <summary> + /// Test Repair MSI Installer. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test. </returns> + [Test] + public async Task RepairMSIInstaller() + { + string msiInstallerPackageId = "AppInstallerTest.TestMsiRepair"; + var searchResult = await this.FindAndInstallPackage(msiInstallerPackageId, this.installDir, null); + + // Note: The 'msiexec repair' command requires the original installer file to be present at the location registered in the ARP (Add/Remove Programs). + // In our test scenario, the MSI installer file is initially placed in a temporary location and then deleted, which can cause the repair operation to fail. + // To work around this, we copy the installer file to the ARP source directory before running the repair command. + // A more permanent solution would be to modify the MSI installer to cache the installer file in a known location and register that location as the installer source. + // This would allow the 'msiexec repair' command to function as expected. + string installerSourceDir = TestCommon.CopyInstallerFileToARPInstallSourceDirectory(TestCommon.GetTestDataFile("AppInstallerTestMsiInstallerV2.msi"), Constants.MsiInstallerProductCode, true); + + // Repair the package + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); + + // Cleanup + Assert.True(TestCommon.VerifyTestMsiInstalledAndCleanup(this.installDir)); + + if (installerSourceDir != null && Directory.Exists(installerSourceDir)) + { + Directory.Delete(installerSourceDir, true); + } + } + + /// <summary> + /// Test Repair MSIX Installer. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairNonStoreMSIXPackage() + { + string msixPackageId = "AppInstallerTest.TestMsixInstaller"; + var searchResult = await this.FindAndInstallPackage(msixPackageId, this.installDir, null); + + // Repair the package + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); + + // Cleanup + Assert.True(TestCommon.VerifyTestMsixInstalledAndCleanup()); + } + + /// <summary> + /// Test MSIX non-store package repair with machine scope. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairNonStoreMsixPackageWithMachineScope() + { + var findPackages = this.FindAllPackages(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.ContainsCaseInsensitive, "Microsoft.Paint"); + + if (findPackages == null || findPackages.Count == 0) + { + Assert.Ignore("Test skipped as Microsoft.Paint_8wekyb3d8bbwe can't be found."); + } + + var searchResult = findPackages.First(); + + if (searchResult == null || searchResult.CatalogPackage == null || searchResult.CatalogPackage.InstalledVersion == null) + { + Assert.Ignore("Test skipped as Microsoft.Paint_8wekyb3d8bbwe is not installed."); + } + + // Repair the package + var repairOptions = this.TestFactory.CreateRepairOptions(); + repairOptions.PackageRepairScope = PackageRepairScope.System; + + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_INSTALL_SYSTEM_NOT_SUPPORTED); + } + + /// <summary> + /// Test repair of a Burn installer that has a "modify" repair behavior specified in the manifest. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairBurnInstallerWithModifyBehavior() + { + var replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestModifyRepair", useHKLM: true); + var searchResult = await this.FindAndInstallPackage(Constants.ModifyRepairInstaller, this.installDir, replaceInstallerArguments.ToString()); + + // Repair the package + var repairOptions = this.TestFactory.CreateRepairOptions(); + repairOptions.PackageRepairMode = PackageRepairMode.Silent; + + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.Ok); + + // Cleanup + Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Modify Repair operation")); + } + + /// <summary> + /// Tests the repair operation of a Burn installer that was installed in user scope but is being repaired in an admin context. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairBurnInstallerInAdminContextWithUserScopeInstall() + { + if (this.TestFactory.Context != ClsidContext.InProc) + { + Assert.Ignore("Test is only applicable for InProc context."); + } + + string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestUserScopeInstallRepairInAdminContext"); + var searchResult = await this.FindAndInstallPackage("AppInstallerTest.TestUserScopeInstallRepairInAdminContext", this.installDir, replaceInstallerArguments); + + // Repair the package + var repairOptions = this.TestFactory.CreateRepairOptions(); + repairOptions.PackageRepairMode = PackageRepairMode.Silent; + + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED); + + // Cleanup + TestCommon.CleanupTestExeAndDirectory(this.installDir); + } + + /// <summary> + /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest and NoModify ARP flag set. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairBurnInstallerWithModifyBehaviorAndNoModifyFlag() + { + string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestModifyRepairWithNoModify", useHKLM: true, noModify: true); + var searchResult = await this.FindAndInstallPackage("AppInstallerTest.TestModifyRepairWithNoModify", this.installDir, replaceInstallerArguments); + + // Repair the package + var repairOptions = this.TestFactory.CreateRepairOptions(); + repairOptions.PackageRepairMode = PackageRepairMode.Silent; + + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED); + + // Cleanup + TestCommon.CleanupTestExeAndDirectory(this.installDir); + } + + /// <summary> + /// Tests the scenario where the repair operation is not supported for Portable Installer type. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairOperationNotSupportedForPortableInstaller() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + var searchResult = await this.FindAndInstallPackage(packageId, null, null); + + // Repair the package + var repairOptions = this.TestFactory.CreateRepairOptions(); + repairOptions.PackageRepairMode = PackageRepairMode.Silent; + + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED); + + // If no location specified, default behavior is to create a package directory with the name "{packageId}_{sourceId}" + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); + } + + /// <summary> + /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairExeInstallerWithUninstallerBehavior() + { + string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "UninstallerRepair", useHKLM: true); + var searchResult = await this.FindAndInstallPackage("AppInstallerTest.UninstallerRepair", this.installDir, replaceInstallerArguments); + + // Repair the package + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); + + // Cleanup + Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Uninstaller Repair operation")); + } + + /// <summary> + /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest and NoRepair ARP flag set. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairExeInstallerWithUninstallerBehaviorAndNoRepairFlag() + { + string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "UninstallerRepairWithNoRepair", useHKLM: true, noRepair: true); + var searchResult = await this.FindAndInstallPackage("AppInstallerTest.UninstallerRepairWithNoRepair", this.installDir, replaceInstallerArguments); + + // Repair the package + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED); + + // Cleanup + TestCommon.CleanupTestExeAndDirectory(this.installDir); + } + + /// <summary> + /// Test repair of a Nullsoft installer that has a "uninstaller" repair behavior specified in the manifest. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairNullsoftInstallerWithUninstallerBehavior() + { + string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "NullsoftUninstallerRepair", useHKLM: true); + var searchResult = await this.FindAndInstallPackage("AppInstallerTest.NullsoftUninstallerRepair", this.installDir, replaceInstallerArguments.ToString()); + + // Repair the package + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); + + // Cleanup + Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Uninstaller Repair operation")); + } + + /// <summary> + /// Test repair of a Inno installer that has a "installer" repair behavior specified in the manifest. + /// </summary> + /// <returns>representing the asynchronous unit test.</returns> + [Test] + public async Task RepairInnoInstallerWithInstallerRepairBehavior() + { + string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestInstallerRepair", useHKLM: true); + var searchResult = await this.FindAndInstallPackage("AppInstallerTest.TestInstallerRepair", this.installDir, replaceInstallerArguments); + + // Repair the package + await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); + + // Cleanup + Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Installer Repair operation")); + } + + private async Task<MatchResult> FindAndInstallPackage(string packageId, string installDir, string replacementInstallerArguments) + { + // Find a package + var searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); + + // Configure installation + var installOptions = this.TestFactory.CreateInstallOptions(); + installOptions.PackageInstallMode = PackageInstallMode.Silent; + + if (!string.IsNullOrEmpty(installDir)) + { + installOptions.PreferredInstallLocation = installDir; + } + + if (!string.IsNullOrEmpty(replacementInstallerArguments)) + { + installOptions.ReplacementInstallerArguments = replacementInstallerArguments; + } + + // Install the package + var installResult = await this.packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); + Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); + + // Find package again, but this time it should be detected as installed + searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); + Assert.NotNull(searchResult.CatalogPackage.InstalledVersion); + + // Return the search result + return searchResult; + } + + private async Task RepairPackageAndValidateStatus(CatalogPackage package, RepairOptions repairOptions, RepairResultStatus expectedStatus, int expectedErrorCode = 0) + { + var repairResult = await this.packageManager.RepairPackageAsync(package, repairOptions); + Assert.AreEqual(expectedStatus, repairResult.Status); + + if (expectedStatus != RepairResultStatus.Ok) + { + Assert.AreEqual(expectedErrorCode, repairResult.ExtendedErrorCode.HResult); + } + } + + private string GetReplacementArguments(string installDir, string version, string displayName, bool useHKLM = false, bool noModify = false, bool noRepair = false) + { + var replacementArguments = new StringBuilder($"/InstallDir {installDir} /Version {version} /DisplayName {displayName}"); + + // Machine scope install. + if (useHKLM) + { + replacementArguments.Append($" /UseHKLM"); + } + + // Instructs test installer to set NoModify ARP flag. + if (noModify) + { + replacementArguments.Append($" /NoModify"); + } + + // Instructs test installer to set NoRepair ARP flag. + if (noRepair) + { + replacementArguments.Append($" /NoRepair"); + } + + return replacementArguments.ToString(); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/RepairCommand.cs b/src/AppInstallerCLIE2ETests/RepairCommand.cs @@ -94,7 +94,7 @@ namespace AppInstallerCLIE2ETests Assert.Ignore("Test skipped as Microsoft.Paint_8wekyb3d8bbwe is not installed."); } - Assert.True(result.StdOut.Contains("Microsoft.Paint_8wekyb3d8bbwe")); + Assert.True(result.StdOut.Contains("Microsoft.Paint")); result = TestCommon.RunAICLICommand("repair", "Microsoft.Paint_8wekyb3d8bbwe --scope machine"); Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALL_SYSTEM_NOT_SUPPORTED, result.ExitCode); diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MissingRepairBehavior.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MissingRepairBehavior.yaml @@ -9,7 +9,6 @@ Installers: InstallerType: burn InstallerSha256: <EXEHASH> ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir <INSTALLPATH> Custom: /Version 2.0.0.0 /DisplayName TestMissingRepairBehavior /UseHKLM diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: burn InstallerSha256: <EXEHASH> ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir <INSTALLPATH> Custom: /Version 2.0.0.0 /DisplayName TestModifyRepair /UseHKLM diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepairWithNoModify.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepairWithNoModify.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: burn InstallerSha256: <EXEHASH> ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir <INSTALLPATH> Custom: /Version 2.0.0.0 /DisplayName TestModifyRepairWithNoModify /UseHKLM /NoModify diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: exe InstallerSha256: <EXEHASH> ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir <INSTALLPATH> Custom: /Version 2.0.0.0 /DisplayName UninstallerRepair /UseHKLM diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepairWithNoRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepairWithNoRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: exe InstallerSha256: <EXEHASH> ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir <INSTALLPATH> Custom: /Version 2.0.0.0 /DisplayName UninstallerRepairWithNoRepair /UseHKLM /NoRepair diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestInnoInstaller.InstallerRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestInnoInstaller.InstallerRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: inno InstallerSha256: <EXEHASH> ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir <INSTALLPATH> Custom: /Version 2.0.0.0 /DisplayName TestInstallerRepair /UseHKLM diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestNullsoftInstaller.UninstallerRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestNullsoftInstaller.UninstallerRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: exe InstallerSha256: <EXEHASH> ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir <INSTALLPATH> Custom: /Version 2.0.0.0 /DisplayName NullsoftUninstallerRepair /UseHKLM diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -78,6 +78,8 @@ </com:Class> <com:Class Id ="6484A61D-50FA-41F0-B71E-F4370C6EB37C" DisplayName="AuthenticationArguments Server"> </com:Class> + <com:Class Id ="E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03" DisplayName="RepairOptions Server"> + </com:Class> </com:ExeServer> </com:ComServer> </com:Extension> diff --git a/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest b/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest @@ -43,6 +43,9 @@ clsid="{80CF9D63-5505-4342-B9B4-BB87895CA8BB}" threadingModel="Both" description="PackageManagerSettings"/> + <comClass + clsid="{30C024C4-852C-4DD4-9810-1348C51EF9BB}" + threadingModel="Both" + description="RepairOptions"/> </file> - </assembly> diff --git a/src/Microsoft.Management.Deployment.OutOfProc/Factory.cpp b/src/Microsoft.Management.Deployment.OutOfProc/Factory.cpp @@ -21,6 +21,7 @@ namespace Microsoft::Management::Deployment::OutOfProc constexpr CLSID CLSID_CreateCompositePackageCatalogOptions = { 0x526534B8, 0x7E46, 0x47C8, { 0x84, 0x16, 0xB1, 0x68, 0x5C, 0x32, 0x7D, 0x37 } }; //526534B8-7E46-47C8-8416-B1685C327D37 constexpr CLSID CLSID_DownloadOptions = { 0x4CBABE76, 0x7322, 0x4BE4, { 0x9C, 0xEA, 0x25, 0x89, 0xA8, 0x06, 0x82, 0xDC } }; //4CBABE76-7322-4BE4-9CEA-2589A80682DC constexpr CLSID CLSID_AuthenticationArguments = { 0xBA580786, 0xBDE3, 0x4F6C, { 0xB8, 0xF3, 0x44, 0x69, 0x8A, 0xC8, 0x71, 0x1A } }; //BA580786-BDE3-4F6C-B8F3-44698AC8711A + constexpr CLSID CLSID_RepairOptions = { 0x0498F441, 0x3097, 0x455F, { 0x9C, 0xAF, 0x14, 0x8F, 0x28, 0x29, 0x38, 0x65 } }; //0498F441-3097-455F-9CAF-148F28293865 #else constexpr CLSID CLSID_PackageManager = { 0x74CB3139, 0xB7C5, 0x4B9E, { 0x93, 0x88, 0xE6, 0x61, 0x6D, 0xEA, 0x28, 0x8C } }; //74CB3139-B7C5-4B9E-9388-E6616DEA288C constexpr CLSID CLSID_InstallOptions = { 0x44FE0580, 0x62F7, 0x44D4, { 0x9E, 0x91, 0xAA, 0x96, 0x14, 0xAB, 0x3E, 0x86 } }; //44FE0580-62F7-44D4-9E91-AA9614AB3E86 @@ -30,6 +31,7 @@ namespace Microsoft::Management::Deployment::OutOfProc constexpr CLSID CLSID_CreateCompositePackageCatalogOptions = { 0xEE160901, 0xB317, 0x4EA7, { 0x9C, 0xC6, 0x53, 0x55, 0xC6, 0xD7, 0xD8, 0xA7 } }; //EE160901-B317-4EA7-9CC6-5355C6D7D8A7 constexpr CLSID CLSID_DownloadOptions = { 0x8EF324ED, 0x367C, 0x4880, { 0x83, 0xE5, 0xBB, 0x2A, 0xBD, 0x0B, 0x72, 0xF6 } }; //8EF324ED-367C-4880-83E5-BB2ABD0B72F6 constexpr CLSID CLSID_AuthenticationArguments = { 0x6484A61D, 0x50FA, 0x41F0, { 0xB7, 0x1E, 0xF4, 0x37, 0x0C, 0x6E, 0xB3, 0x7C } }; //6484A61D-50FA-41F0-B71E-F4370C6EB37C + constexpr CLSID CLSID_RepairOptions = { 0xE62BB1E7, 0xC7B2, 0x4AEC, { 0x9E, 0x28, 0xFB, 0x64, 0x9B, 0x30, 0xFF, 0x03 } }; //E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03 #endif struct NameCLSIDPair @@ -38,7 +40,7 @@ namespace Microsoft::Management::Deployment::OutOfProc GUID CLSID; }; - constexpr std::array<NameCLSIDPair, 8> s_nameCLSIDPairs + constexpr std::array<NameCLSIDPair, 9> s_nameCLSIDPairs { NameCLSIDPair{ L"Microsoft.Management.Deployment.PackageManager"sv, CLSID_PackageManager }, NameCLSIDPair{ L"Microsoft.Management.Deployment.InstallOptions"sv, CLSID_InstallOptions }, @@ -48,6 +50,7 @@ namespace Microsoft::Management::Deployment::OutOfProc NameCLSIDPair{ L"Microsoft.Management.Deployment.CreateCompositePackageCatalogOptions"sv, CLSID_CreateCompositePackageCatalogOptions }, NameCLSIDPair{ L"Microsoft.Management.Deployment.DownloadOptions"sv, CLSID_DownloadOptions }, NameCLSIDPair{ L"Microsoft.Management.Deployment.AuthenticationArguments"sv, CLSID_AuthenticationArguments }, + NameCLSIDPair{ L"Microsoft.Management.Deployment.RepairOptions"sv, CLSID_RepairOptions } }; bool IsCLSIDPresent(const GUID& clsid) diff --git a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. namespace Microsoft.Management.Deployment.Projection @@ -114,6 +114,18 @@ namespace Microsoft.Management.Deployment.Projection { [ClsidContext.InProc] = new Guid("80CF9D63-5505-4342-B9B4-BB87895CA8BB"), } + }, + + [typeof(RepairOptions)] = new () + { + ProjectedClassType = typeof(RepairOptions), + InterfaceType = typeof(IRepairOptions), + Clsids = new Dictionary<ClsidContext, Guid>() + { + [ClsidContext.InProc] = new Guid("30C024C4-852C-4DD4-9810-1348C51EF9BB"), + [ClsidContext.OutOfProc] = new Guid("0498F441-3097-455F-9CAF-148F28293865"), + [ClsidContext.OutOfProcDev] = new Guid("E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03"), + } } }; diff --git a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. namespace Microsoft.Management.Deployment.Projection @@ -33,6 +33,8 @@ namespace Microsoft.Management.Deployment.Projection public AuthenticationArguments CreateAuthenticationArguments() => InstanceInitializer.CreateInstance<AuthenticationArguments>(); - public PackageManagerSettings CreatePackageManagerSettings() => InstanceInitializer.CreateInstance<PackageManagerSettings>(); + public PackageManagerSettings CreatePackageManagerSettings() => InstanceInitializer.CreateInstance<PackageManagerSettings>(); + + public RepairOptions CreateRepairOptions() => InstanceInitializer.CreateInstance<RepairOptions>(); } } diff --git a/src/Microsoft.Management.Deployment/ComClsids.cpp b/src/Microsoft.Management.Deployment/ComClsids.cpp @@ -13,7 +13,8 @@ #include "PackageMatchFilter.h" #include "PackageManagerSettings.h" #include "DownloadOptions.h" -#include "AuthenticationArguments.h" +#include "AuthenticationArguments.h" +#include "RepairOptions.h" #pragma warning( pop ) namespace winrt::Microsoft::Management::Deployment @@ -55,10 +56,14 @@ namespace winrt::Microsoft::Management::Deployment else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManagerSettings)) { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManagerSettings); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_RepairOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::RepairOptions); } else { return CLSID_NULL; } } -}- \ No newline at end of file +} diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp @@ -353,6 +353,21 @@ namespace winrt::Microsoft::Management::Deployment::implementation return ::AppInstaller::Manifest::ScopeEnum::Unknown; } + ::AppInstaller::Manifest::ScopeEnum GetManifestRepairScope(winrt::Microsoft::Management::Deployment::PackageRepairScope scope) + { + switch (scope) + { + case winrt::Microsoft::Management::Deployment::PackageRepairScope::Any: + return ::AppInstaller::Manifest::ScopeEnum::Unknown; + case winrt::Microsoft::Management::Deployment::PackageRepairScope::User: + return ::AppInstaller::Manifest::ScopeEnum::User; + case winrt::Microsoft::Management::Deployment::PackageRepairScope::System: + return ::AppInstaller::Manifest::ScopeEnum::Machine; + } + + return ::AppInstaller::Manifest::ScopeEnum::Unknown; + } + winrt::Microsoft::Management::Deployment::ElevationRequirement GetDeploymentElevationRequirement(::AppInstaller::Manifest::ElevationRequirementEnum elevationRequirement) { switch (elevationRequirement) diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h @@ -30,8 +30,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::AuthenticationType GetDeploymentAuthenticationType(::AppInstaller::Authentication::AuthenticationType authType); ::AppInstaller::Authentication::AuthenticationMode GetAuthenticationMode(winrt::Microsoft::Management::Deployment::AuthenticationMode authMode); ::AppInstaller::Authentication::AuthenticationArguments GetAuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments authArgs); + ::AppInstaller::Manifest::ScopeEnum GetManifestRepairScope(winrt::Microsoft::Management::Deployment::PackageRepairScope scope); -#define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_) \ +#define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_, _repairResultStatus_) \ if constexpr (std::is_same_v<TStatus, winrt::Microsoft::Management::Deployment::InstallResultStatus>) \ { \ resultStatus = TStatus::_installResultStatus_; \ @@ -44,6 +45,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { \ resultStatus = TStatus::_downloadResultStatus_; \ } \ + else if constexpr (std::is_same_v<TStatus, winrt::Microsoft::Management::Deployment::RepairResultStatus>) \ + { \ + resultStatus = TStatus::_repairResultStatus_; \ + } \ template <typename TStatus> TStatus GetOperationResultStatus(::AppInstaller::CLI::Workflow::ExecutionStage executionStage, winrt::hresult hresult) @@ -70,19 +75,26 @@ namespace winrt::Microsoft::Management::Deployment::implementation resultStatus = TStatus::InvalidOptions; break; case APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER: - WINGET_GET_OPERATION_RESULT_STATUS(NoApplicableInstallers, InternalError, NoApplicableInstallers); + WINGET_GET_OPERATION_RESULT_STATUS(NoApplicableInstallers, InternalError, NoApplicableInstallers, NoApplicableRepairer); break; case APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE: case APPINSTALLER_CLI_ERROR_UPGRADE_VERSION_UNKNOWN: case APPINSTALLER_CLI_ERROR_UPGRADE_VERSION_NOT_NEWER: - WINGET_GET_OPERATION_RESULT_STATUS(NoApplicableUpgrade, InternalError, InternalError); + WINGET_GET_OPERATION_RESULT_STATUS(NoApplicableUpgrade, InternalError, InternalError, InternalError); break; case APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND: case APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED: - WINGET_GET_OPERATION_RESULT_STATUS(InstallError, UninstallError, InternalError); + WINGET_GET_OPERATION_RESULT_STATUS(InstallError, UninstallError, InternalError, InternalError); + break; + case APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND: + case APPINSTALLER_CLI_ERROR_REPAIR_NOT_APPLICABLE: + case APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED: + case APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED: + case APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED: + WINGET_GET_OPERATION_RESULT_STATUS(InternalError, InternalError, InternalError, RepairError); break; case APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED: - WINGET_GET_OPERATION_RESULT_STATUS(PackageAgreementsNotAccepted, InternalError, PackageAgreementsNotAccepted); + WINGET_GET_OPERATION_RESULT_STATUS(PackageAgreementsNotAccepted, InternalError, PackageAgreementsNotAccepted, PackageAgreementsNotAccepted); break; case APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX: case APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED: @@ -110,13 +122,13 @@ namespace winrt::Microsoft::Management::Deployment::implementation resultStatus = TStatus::CatalogError; break; case ::AppInstaller::CLI::Workflow::ExecutionStage::Download: - WINGET_GET_OPERATION_RESULT_STATUS(DownloadError, InternalError, DownloadError); + WINGET_GET_OPERATION_RESULT_STATUS(DownloadError, InternalError, DownloadError, DownloadError); break; case ::AppInstaller::CLI::Workflow::ExecutionStage::PreExecution: resultStatus = TStatus::InternalError; break; case ::AppInstaller::CLI::Workflow::ExecutionStage::Execution: - WINGET_GET_OPERATION_RESULT_STATUS(InstallError, UninstallError, InternalError); + WINGET_GET_OPERATION_RESULT_STATUS(InstallError, UninstallError, InternalError, RepairError); break; case ::AppInstaller::CLI::Workflow::ExecutionStage::PostExecution: resultStatus = TStatus::InternalError; diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -217,6 +217,8 @@ <ClInclude Include="pch.h" /> <ClInclude Include="Public\CoCreatableMicrosoftManagementDeploymentClass.h" /> <ClInclude Include="Public\ComClsids.h" /> + <ClInclude Include="RepairOptions.h" /> + <ClInclude Include="RepairResult.h" /> <ClInclude Include="SourceAgreement.h" /> <ClInclude Include="UninstallOptions.h" /> <ClInclude Include="UninstallResult.h" /> @@ -257,6 +259,8 @@ <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> + <ClCompile Include="RepairOptions.cpp" /> + <ClCompile Include="RepairResult.cpp" /> <ClCompile Include="SourceAgreement.cpp" /> <ClCompile Include="$(GeneratedFilesDir)module.g.cpp" /> <ClCompile Include="UninstallOptions.cpp" /> diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters @@ -38,6 +38,8 @@ <ClCompile Include="AuthenticationArguments.cpp" /> <ClCompile Include="AuthenticationInfo.cpp" /> <ClCompile Include="MicrosoftEntraIdAuthenticationInfo.cpp" /> + <ClCompile Include="RepairResult.cpp" /> + <ClCompile Include="RepairOptions.cpp" /> </ItemGroup> <ItemGroup> <ClInclude Include="CatalogPackage.h" /> @@ -81,6 +83,8 @@ <ClInclude Include="AuthenticationArguments.h" /> <ClInclude Include="AuthenticationInfo.h" /> <ClInclude Include="MicrosoftEntraIdAuthenticationInfo.h" /> + <ClInclude Include="RepairResult.h" /> + <ClInclude Include="RepairOptions.h" /> </ItemGroup> <ItemGroup> <Midl Include="PackageManager.idl" /> diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -25,6 +25,7 @@ #include "DownloadResult.h" #include "InstallResult.h" #include "UninstallResult.h" +#include "RepairResult.h" #include "PackageCatalogInfo.h" #include "PackageCatalogReference.h" #include "PackageVersionInfo.h" @@ -215,6 +216,13 @@ namespace winrt::Microsoft::Management::Deployment::implementation return *downloadResult; } + winrt::Microsoft::Management::Deployment::RepairResult GetRepairResult(::Workflow::ExecutionStage executionStage, winrt::hresult terminationHR, uint32_t repairError, winrt::hstring correlationData, bool rebootRequired) + { + winrt::Microsoft::Management::Deployment::RepairResultStatus repairResultStatus = GetOperationResultStatus<RepairResultStatus>(executionStage, terminationHR); + auto repairResult = winrt::make_self<wil::details::module_count_wrapper<winrt::Microsoft::Management::Deployment::implementation::RepairResult>>(); + repairResult->Initialize(repairResultStatus, terminationHR, repairError, correlationData, rebootRequired); + return *repairResult; + } template <typename TResult> TResult GetOperationResult(::Workflow::ExecutionStage executionStage, winrt::hresult terminationHR, uint32_t operationError, winrt::hstring correlationData, bool rebootRequired) @@ -231,9 +239,13 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return GetDownloadResult(executionStage, terminationHR, correlationData); } + else if constexpr (std::is_same_v<TResult, winrt::Microsoft::Management::Deployment::RepairResult>) + { + return GetRepairResult(executionStage, terminationHR, operationError, correlationData, rebootRequired); + } } -#define WINGET_GET_PROGRESS_STATE(_installState_, _uninstallState_) \ +#define WINGET_GET_PROGRESS_STATE(_installState_, _uninstallState_, _repairState_) \ if constexpr (std::is_same_v<TState, winrt::Microsoft::Management::Deployment::PackageInstallProgressState>) \ { \ progressState = TState::_installState_; \ @@ -241,7 +253,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation else if constexpr (std::is_same_v<TState, winrt::Microsoft::Management::Deployment::PackageUninstallProgressState>) \ { \ progressState = TState::_uninstallState_; \ - } + } \ + else if constexpr (std::is_same_v<TState, winrt::Microsoft::Management::Deployment::PackageRepairProgressState>) \ + { \ + progressState = TState::_repairState_; \ + } \ template <typename TProgress, typename TState> std::optional<TProgress> GetProgress( @@ -289,7 +305,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation // Wait until installer starts to report operation. break; case ::Workflow::ExecutionStage::Execution: - WINGET_GET_PROGRESS_STATE(Installing, Uninstalling); + WINGET_GET_PROGRESS_STATE(Installing, Uninstalling, Repairing); downloadProgress = 1; if (reportType == ReportType::ExecutionPhaseUpdate) { @@ -317,7 +333,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation { // Send PostInstall progress when it switches to PostExecution phase. reportProgress = true; - WINGET_GET_PROGRESS_STATE(PostInstall, PostUninstall); + WINGET_GET_PROGRESS_STATE(PostInstall, PostUninstall, PostRepair); downloadProgress = 1; operationProgress = 1; } @@ -340,6 +356,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation TProgress progress{ progressState, downloadBytesDownloaded, downloadBytesRequired, downloadProgress }; return progress; } + else if constexpr (std::is_same_v<TProgress, winrt::Microsoft::Management::Deployment::RepairProgress>) + { + TProgress progress{ progressState, operationProgress }; + return progress; + } } else { @@ -548,6 +569,55 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } + void PopulateContextFromRepairOptions( + ::AppInstaller::CLI::Execution::Context* context, + winrt::Microsoft::Management::Deployment::RepairOptions options) + { + if (options) + { + if (!options.LogOutputPath().empty()) + { + context->Args.AddArg(Execution::Args::Type::Log, ::AppInstaller::Utility::ConvertToUTF8(options.LogOutputPath())); + context->Args.AddArg(Execution::Args::Type::VerboseLogs); + } + + if (options.PackageRepairMode() == PackageRepairMode::Interactive) + { + context->Args.AddArg(Execution::Args::Type::Interactive); + } + else if (options.PackageRepairMode() == PackageRepairMode::Silent) + { + context->Args.AddArg(Execution::Args::Type::Silent); + } + + if (options.AcceptPackageAgreements()) + { + context->Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); + } + + if (options.AllowHashMismatch()) + { + context->Args.AddArg(Execution::Args::Type::HashOverride); + } + + if (options.BypassIsStoreClientBlockedPolicyCheck()) + { + context->SetFlags(Execution::ContextFlag::BypassIsStoreClientBlockedPolicyCheck); + } + + if (options.Force()) + { + context->Args.AddArg(Execution::Args::Type::Force); + } + + auto repairScope = GetManifestRepairScope(options.PackageRepairScope()); + if (repairScope != ::AppInstaller::Manifest::ScopeEnum::Unknown) + { + context->Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(repairScope)); + } + } + } + template <typename TOptions> std::unique_ptr<COMContext> CreateContextFromOperationOptions( TOptions options, @@ -571,6 +641,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { PopulateContextFromDownloadOptions(context.get(), options); } + else if constexpr (std::is_same_v<TOptions, winrt::Microsoft::Management::Deployment::RepairOptions>) + { + PopulateContextFromRepairOptions(context.get(), options); + } return context; } @@ -697,6 +771,24 @@ namespace winrt::Microsoft::Management::Deployment::implementation return Execution::OrchestratorQueueItemFactory::CreateItemForDownload(std::wstring{ package.Id() }, std::wstring{ packageVersionInfo.PackageCatalog().Info().Id() }, std::move(comContext)); } + std::unique_ptr<Execution::OrchestratorQueueItem> CreateQueueItemForRepair( + std::unique_ptr<::AppInstaller::CLI::Execution::COMContext> comContext, + winrt::Microsoft::Management::Deployment::CatalogPackage package, + winrt::Microsoft::Management::Deployment::RepairOptions options) + { + // Add installed version + AddInstalledVersionToContext(package.InstalledVersion(), comContext.get()); + + // Add Package which is used to co-relate installed package with available package for repair + winrt::Microsoft::Management::Deployment::implementation::CatalogPackage* catalogPackageImpl = get_self<winrt::Microsoft::Management::Deployment::implementation::CatalogPackage>(package); + std::shared_ptr<::AppInstaller::Repository::ICompositePackage> internalPackage = catalogPackageImpl->GetRepositoryPackage(); + comContext->Add<AppInstaller::CLI::Execution::Data::Package>(internalPackage); + + comContext->SetFlags(AppInstaller::CLI::Execution::ContextFlag::InstallerExecutionUseRepair); + + return Execution::OrchestratorQueueItemFactory::CreateItemForRepair(std::wstring{ package.Id() }, std::wstring{ package.InstalledVersion().PackageCatalog().Info().Id() }, std::move(comContext)); + } + template <typename TResult, typename TProgress, typename TOptions, typename TProgressState> winrt::Windows::Foundation::IAsyncOperationWithProgress<TResult, TProgress> GetPackageOperation( bool canCancelQueueItem, @@ -737,6 +829,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { queueItem = CreateQueueItemForDownload(std::move(comContext), package, options); } + else if constexpr (std::is_same_v<TOptions, winrt::Microsoft::Management::Deployment::RepairOptions>) + { + queueItem = CreateQueueItemForRepair(std::move(comContext), package, options); + } Execution::ContextOrchestrator::Instance().EnqueueAndRunItem(queueItem); @@ -755,6 +851,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation TProgress queuedProgress{ TProgressState::Queued, 0 }; report_progress(queuedProgress); } + else if constexpr (std::is_same_v<TProgress, winrt::Microsoft::Management::Deployment::PackageRepairProgressState>) + { + TProgress queuedProgress{ TProgressState::Queued, 0 }; + report_progress(queuedProgress); + } } { // correlation data is not passed in when retrieving an existing queue item, so get it from the existing context. @@ -1066,5 +1167,35 @@ namespace winrt::Microsoft::Management::Deployment::implementation return GetPackageOperation<Deployment::DownloadResult, Deployment::PackageDownloadProgress, Deployment::DownloadOptions, Deployment::PackageDownloadProgressState>(true, std::move(queueItem)); } +#define WINGET_RETURN_REPAIR_RESULT_HR_IF(hr, boolVal) { if(boolVal) { return GetEmptyAsynchronousResultForOperation<Deployment::RepairResult, Deployment::RepairProgress>(hr, correlationData); }} +#define WINGET_RETURN_REPAIR_RESULT_HR_IF_FAILED(hr) { WINGET_RETURN_REPAIR_RESULT_HR_IF(hr, FAILED(hr)) } + + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::RepairResult, winrt::Microsoft::Management::Deployment::RepairProgress> PackageManager::RepairPackageAsync(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::RepairOptions options) + { + hstring correlationData = (options) ? options.CorrelationData() : L""; + + // options and catalog can both be null, package must be set. + WINGET_RETURN_REPAIR_RESULT_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, !package); + // the package should have an installed version to be repaired. + WINGET_RETURN_REPAIR_RESULT_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, !package.InstalledVersion()); + + HRESULT hr = S_OK; + std::wstring callerProcessInfoString; + try + { + // Check for permissions and get caller info for telemetry. + // This must be done before any co_awaits since it requires info from the rpc caller thread. + auto [hrGetCallerId, callerProcessId] = GetCallerProcessId(); + WINGET_RETURN_REPAIR_RESULT_HR_IF_FAILED(hrGetCallerId); + WINGET_RETURN_REPAIR_RESULT_HR_IF_FAILED(EnsureProcessHasCapability(Capability::PackageManagement, callerProcessId)); + callerProcessInfoString = TryGetCallerProcessInfo(callerProcessId); + } + WINGET_CATCH_STORE(hr, APPINSTALLER_CLI_ERROR_COMMAND_FAILED); + WINGET_RETURN_REPAIR_RESULT_HR_IF_FAILED(hr); + + return GetPackageOperation<Deployment::RepairResult, Deployment::RepairProgress, Deployment::RepairOptions, Deployment::PackageRepairProgressState>( + true /*canCancelQueueItem*/, nullptr /*queueItem*/, package, options, std::move(callerProcessInfoString)); + } + CoCreatableMicrosoftManagementDeploymentClass(PackageManager); } diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h @@ -41,6 +41,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation DownloadPackageAsync(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::DownloadOptions options); winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::DownloadResult, winrt::Microsoft::Management::Deployment::PackageDownloadProgress> GetDownloadProgress(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PackageCatalogInfo catalogInfo); + // Contract 11.0 + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::RepairResult, winrt::Microsoft::Management::Deployment::RepairProgress> + RepairPackageAsync(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::RepairOptions options); }; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -151,6 +151,73 @@ namespace Microsoft.Management.Deployment UInt32 UninstallerErrorCode { get; }; } + /// State of the repair + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + enum PackageRepairProgressState + { + /// The repair is queued but not yet active. Cancellation of the IAsyncOperationWithProgress in this + /// state will prevent the package from repairing. + Queued, + /// The repair is in progress. Cancellation of the IAsyncOperationWithProgress in this state will not + /// stop the repair or the post repair steps. + Repairing, + /// The repair has completed and cleanup actions are in progress. Cancellation of the + /// IAsyncOperationWithProgress in this state will not stop cleanup or roll back the repair. + PostRepair, + /// The operation has completed. + Finished, + }; + + /// Progress object for the repair + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + struct RepairProgress + { + /// State of the repair + PackageRepairProgressState State; + + /// Repair percentage if known. + Double RepairCompletionProgress; + }; + + /// Status of the repair call + /// Implementation Note: Errors mapped from AppInstallerErrors.h + /// DESIGN NOTE: RepairResultStatus from AppInstallerErrors.h is not implemented in V1. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + enum RepairResultStatus + { + Ok, + BlockedByPolicy, + CatalogError, + DownloadError, + InternalError, + InvalidOptions, + RepairError, + ManifestError, + NoApplicableRepairer, + PackageAgreementsNotAccepted, + }; + + /// Result of the repair + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + runtimeclass RepairResult + { + /// Used by a caller to correlate the repair with a caller's data. + String CorrelationData { get; }; + + /// Whether a restart is required to complete the repair. + Boolean RebootRequired { get; }; + + /// Batched error code, example APPINSTALLER_CLI_ERROR_SHELLEXEC_INSTALL_FAILED + RepairResultStatus Status { get; }; + + /// The error code of the overall operation. + HRESULT ExtendedErrorCode { get; }; + + /// The error code from the repair attempt. Only valid if the Status is RepairError. + /// This value's meaning will require knowledge of the specific repairer or repair technology. + UInt32 RepairerErrorCode { get; }; + } + /// State of the download [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] enum PackageDownloadProgressState @@ -1077,6 +1144,64 @@ namespace Microsoft.Management.Deployment String CorrelationData; } + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + enum PackageRepairMode + { + /// The default experience for the installer. Installer may show some UI. + Default, + /// Runs the installer in silent mode. This suppresses the installer's UI to the extent + /// possible (installer may still show some required UI). + Silent, + /// Runs the installer in interactive mode. + Interactive, + }; + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + enum PackageRepairScope + { + /// Use default repair behavior. + Any, + /// Repair for current user. Currently only applicable to msix. + User, + /// Repair for all users. + System, + }; + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + runtimeclass RepairOptions + { + RepairOptions(); + + /// Optionally specifies the version from the package to repair. If unspecified the version matching + /// CatalogPackage.GetLatestVersion() is used. + PackageVersionId PackageVersionId; + + /// The package Repair scope. + PackageRepairScope PackageRepairScope; + + /// The package repair mode. + PackageRepairMode PackageRepairMode; + + /// Optional parameter specifying Accept the package agreements required for download. + Boolean AcceptPackageAgreements; + + /// Used by a caller to correlate the repair with a caller's data. + /// The string must be JSON encoded. + String CorrelationData; + + /// Continues the download even if the hash in the catalog does not match the linked installer used for repair. + Boolean AllowHashMismatch; + + /// Directs the logging to a log file. If provided, the installer must have write access to the file + String LogOutputPath; + + /// Force the operation to continue upon non security related failures. + Boolean Force; + + /// Bypasses the Disabled Store Policy + Boolean BypassIsStoreClientBlockedPolicyCheck; + } + /// IMPLEMENTATION NOTE: Documentation from AppInstaller::Manifest::Documentation [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 6)] runtimeclass Documentation @@ -1262,6 +1387,12 @@ namespace Microsoft.Management.Deployment // Get download progress Windows.Foundation.IAsyncOperationWithProgress<DownloadResult, PackageDownloadProgress> GetDownloadProgress(CatalogPackage package, PackageCatalogInfo catalogInfo); } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + { + // Repair the specified package + Windows.Foundation.IAsyncOperationWithProgress<RepairResult, RepairProgress> RepairPackageAsync(CatalogPackage package, RepairOptions options); + } } /// Global settings for PackageManager operations. @@ -1351,5 +1482,9 @@ namespace Microsoft.Management.Deployment interface Windows.Foundation.Collections.IVectorView<MicrosoftEntraIdAuthenticationInfo>; interface Windows.Foundation.Collections.IVector<AuthenticationInfo>; interface Windows.Foundation.Collections.IVectorView<AuthenticationInfo>; + interface Windows.Foundation.Collections.IVector<RepairOptions>; + interface Windows.Foundation.Collections.IVectorView<RepairOptions>; + interface Windows.Foundation.Collections.IVector<RepairResult>; + interface Windows.Foundation.Collections.IVectorView<RepairResult>; } } diff --git a/src/Microsoft.Management.Deployment/Public/ComClsids.h b/src/Microsoft.Management.Deployment/Public/ComClsids.h @@ -14,6 +14,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_ConfigurationStaticFunctions "73D763B7-2937-432F-A97A-D98A4A596126" #define WINGET_OUTOFPROC_COM_CLSID_DownloadOptions "4CBABE76-7322-4BE4-9CEA-2589A80682DC" #define WINGET_OUTOFPROC_COM_CLSID_AuthenticationArguments "BA580786-BDE3-4F6C-B8F3-44698AC8711A" +#define WINGET_OUTOFPROC_COM_CLSID_RepairOptions "0498F441-3097-455F-9CAF-148F28293865" #else #define WINGET_OUTOFPROC_COM_CLSID_PackageManager "74CB3139-B7C5-4B9E-9388-E6616DEA288C" #define WINGET_OUTOFPROC_COM_CLSID_FindPackagesOptions "1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96" @@ -24,6 +25,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_ConfigurationStaticFunctions "C9ED7917-66AB-4E31-A92A-F65F18EF7933" #define WINGET_OUTOFPROC_COM_CLSID_DownloadOptions "8EF324ED-367C-4880-83E5-BB2ABD0B72F6" #define WINGET_OUTOFPROC_COM_CLSID_AuthenticationArguments "6484A61D-50FA-41F0-B71E-F4370C6EB37C" +#define WINGET_OUTOFPROC_COM_CLSID_RepairOptions "E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03" #endif // Clsids only used in in-proc invocation @@ -41,6 +43,7 @@ namespace winrt::Microsoft::Management::Deployment const CLSID WINGET_INPROC_COM_CLSID_PackageManagerSettings = { 0x80CF9D63, 0x5505, 0x4342, 0xB9, 0xB4, 0xBB, 0x87, 0x89, 0x5C, 0xA8, 0xBB }; // 80CF9D63-5505-4342-B9B4-BB87895CA8BB const CLSID WINGET_INPROC_COM_CLSID_DownloadOptions = { 0x4288DF96, 0xFDC9, 0x4B68, 0xB4, 0x03, 0x19, 0x3D, 0xBB, 0xF5, 0x6A, 0x24 }; // 4288DF96-FDC9-4B68-B403-193DBBF56A24 const CLSID WINGET_INPROC_COM_CLSID_AuthenticationArguments = { 0x8D593114, 0x1CF1, 0x43B9, 0x87, 0x22, 0x4D, 0xBB, 0x30, 0x10, 0x32, 0x96 }; // 8D593114-1CF1-43B9-8722-4DBB30103296 + const CLSID WINGET_INPROC_COM_CLSID_RepairOptions = { 0x30c024c4, 0x852c, 0x4dd4, 0x98, 0x10, 0x13, 0x48, 0xc5, 0x1e, 0xf9, 0xbb }; // {30C024C4-852C-4DD4-9810-1348C51EF9BB} CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid); -}- \ No newline at end of file +} diff --git a/src/Microsoft.Management.Deployment/RepairOptions.cpp b/src/Microsoft.Management.Deployment/RepairOptions.cpp @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#pragma warning( push ) +#pragma warning ( disable : 4467 6388) +// 6388 Allow CreateInstance. +#include <wil\cppwinrt_wrl.h> +// 4467 Allow use of uuid attribute for com object creation. +#include "RepairOptions.h" +#pragma warning( pop ) +#include "RepairOptions.g.cpp" +#include "Converters.h" +#include "Helpers.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + RepairOptions::RepairOptions() + { + } + + winrt::Microsoft::Management::Deployment::PackageVersionId RepairOptions::PackageVersionId() + { + return m_packageVersionId; + } + void RepairOptions::PackageVersionId(winrt::Microsoft::Management::Deployment::PackageVersionId const& value) + { + m_packageVersionId = value; + } + + winrt::Microsoft::Management::Deployment::PackageRepairScope RepairOptions::PackageRepairScope() + { + return m_packageRepairScope; + } + + void RepairOptions::PackageRepairScope(winrt::Microsoft::Management::Deployment::PackageRepairScope const& value) + { + m_packageRepairScope = value; + } + + winrt::Microsoft::Management::Deployment::PackageRepairMode RepairOptions::PackageRepairMode() + { + return m_packageRepairMode; + } + + void RepairOptions::PackageRepairMode(winrt::Microsoft::Management::Deployment::PackageRepairMode const& value) + { + m_packageRepairMode = value; + } + + bool RepairOptions::AcceptPackageAgreements() + { + return m_acceptPackageAgreements; + } + + void RepairOptions::AcceptPackageAgreements(bool value) + { + m_acceptPackageAgreements = value; + } + + hstring RepairOptions::LogOutputPath() + { + return hstring(m_logOutputPath); + } + + void RepairOptions::LogOutputPath(hstring const& value) + { + m_logOutputPath = value; + } + + hstring RepairOptions::CorrelationData() + { + return hstring(m_correlationData); + } + + void RepairOptions::CorrelationData(hstring const& value) + { + m_correlationData = value; + } + + bool RepairOptions::AllowHashMismatch() + { + return m_allowHashMismatch; + } + + void RepairOptions::AllowHashMismatch(bool value) + { + m_allowHashMismatch = value; + } + + bool RepairOptions::BypassIsStoreClientBlockedPolicyCheck() + { + return m_bypassIsStoreClientBlockedPolicyCheck; + } + + void RepairOptions::BypassIsStoreClientBlockedPolicyCheck(bool value) + { + m_bypassIsStoreClientBlockedPolicyCheck = value; + } + + bool RepairOptions::Force() + { + return m_force; + } + + void RepairOptions::Force(bool value) + { + m_force = value; + } + + CoCreatableMicrosoftManagementDeploymentClass(RepairOptions); +} diff --git a/src/Microsoft.Management.Deployment/RepairOptions.h b/src/Microsoft.Management.Deployment/RepairOptions.h @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "RepairOptions.g.h" +#include "Public/ComClsids.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + [uuid(WINGET_OUTOFPROC_COM_CLSID_RepairOptions)] + struct RepairOptions : RepairOptionsT<RepairOptions> + { + RepairOptions(); + + winrt::Microsoft::Management::Deployment::PackageVersionId PackageVersionId(); + void PackageVersionId(winrt::Microsoft::Management::Deployment::PackageVersionId const& value); + winrt::Microsoft::Management::Deployment::PackageRepairScope PackageRepairScope(); + void PackageRepairScope(winrt::Microsoft::Management::Deployment::PackageRepairScope const& value); + winrt::Microsoft::Management::Deployment::PackageRepairMode PackageRepairMode(); + void PackageRepairMode(winrt::Microsoft::Management::Deployment::PackageRepairMode const& value); + bool AcceptPackageAgreements(); + void AcceptPackageAgreements(bool value); + hstring LogOutputPath(); + void LogOutputPath(hstring const& value); + hstring CorrelationData(); + void CorrelationData(hstring const& value); + bool AllowHashMismatch(); + void AllowHashMismatch(bool value); + bool BypassIsStoreClientBlockedPolicyCheck(); + void BypassIsStoreClientBlockedPolicyCheck(bool value); + bool Force(); + void Force(bool value); + + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::PackageVersionId m_packageVersionId{ nullptr }; + bool m_acceptPackageAgreements = false; + bool m_allowHashMismatch = false; + bool m_bypassIsStoreClientBlockedPolicyCheck = false; + bool m_force = false; + std::wstring m_logOutputPath = L""; + std::wstring m_correlationData = L""; + winrt::Microsoft::Management::Deployment::PackageRepairScope m_packageRepairScope = winrt::Microsoft::Management::Deployment::PackageRepairScope::Any; + winrt::Microsoft::Management::Deployment::PackageRepairMode m_packageRepairMode = winrt::Microsoft::Management::Deployment::PackageRepairMode::Default; +#endif + }; +} + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) +namespace winrt::Microsoft::Management::Deployment::factory_implementation +{ + struct RepairOptions : RepairOptionsT<RepairOptions, implementation::RepairOptions> + { + }; +} +#endif diff --git a/src/Microsoft.Management.Deployment/RepairResult.cpp b/src/Microsoft.Management.Deployment/RepairResult.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "RepairResult.h" +#include "RepairResult.g.cpp" +#include <wil\cppwinrt_wrl.h> + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + void RepairResult::Initialize( + winrt::Microsoft::Management::Deployment::RepairResultStatus status, + winrt::hresult extendedErrorCode, + uint32_t repairerErrorCode, + hstring const& correlationData, + bool rebootRequired) + { + m_status = status; + m_extendedErrorCode = extendedErrorCode; + m_repairerErrorCode = repairerErrorCode; + m_correlationData = correlationData; + m_rebootRequired = rebootRequired; + } + hstring RepairResult::CorrelationData() + { + return hstring(m_correlationData); + } + bool RepairResult::RebootRequired() + { + return m_rebootRequired; + } + winrt::Microsoft::Management::Deployment::RepairResultStatus RepairResult::Status() + { + return m_status; + } + winrt::hresult RepairResult::ExtendedErrorCode() + { + return m_extendedErrorCode; + } + uint32_t RepairResult::RepairerErrorCode() + { + return m_repairerErrorCode; + } +} diff --git a/src/Microsoft.Management.Deployment/RepairResult.h b/src/Microsoft.Management.Deployment/RepairResult.h @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "RepairResult.g.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct RepairResult : RepairResultT<RepairResult> + { + RepairResult() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize( + winrt::Microsoft::Management::Deployment::RepairResultStatus status, + winrt::hresult extendedErrorCode, + uint32_t repairerErrorCode, + hstring const& correlationData, + bool rebootRequired); +#endif + + hstring CorrelationData(); + bool RebootRequired(); + winrt::Microsoft::Management::Deployment::RepairResultStatus Status(); + winrt::hresult ExtendedErrorCode(); + uint32_t RepairerErrorCode(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + std::wstring m_correlationData = L""; + bool m_rebootRequired = false; + winrt::Microsoft::Management::Deployment::RepairResultStatus m_status = winrt::Microsoft::Management::Deployment::RepairResultStatus::Ok; + winrt::hresult m_extendedErrorCode = S_OK; + uint32_t m_repairerErrorCode = 0; +#endif + }; +} diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md @@ -1,4 +1,4 @@ ---- +--- Module Name: Microsoft.WinGet.Client ms.date: 08/01/2024 Module Guid: e11157e2-cd24-4250-83b8-c6654ea4926a @@ -52,6 +52,9 @@ Install a WinGet Package. ### [Remove-WinGetSource](Remove-WinGetSource.md) Removes a configured source. +### [Repair-WinGetPackage](Repair-WinGetPackage.md) +Repairs a WinGet Package. + ### [Repair-WinGetPackageManager](Repair-WinGetPackageManager.md) Repairs the WinGet client. diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackage.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackage.md @@ -0,0 +1,303 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/26/2024 +online version: +schema: 2.0.0 +--- + +# Repair-WinGetPackage + +## SYNOPSIS +Repairs a WinGet Package. + +## SYNTAX + +### FoundSet (Default) +``` +Repair-WinGetPackage [-Mode <PSPackageRepairMode>] [-Log <String>] [-Version <String>] [-Id <String>] + [-Name <String>] [-Moniker <String>] [-Source <String>] [[-Query] <String[]>] + [-MatchOption <PSPackageFieldMatchOption>] [-ProgressAction <ActionPreference>] [-WhatIf] [-Confirm] + [<CommonParameters>] +``` + +### GivenSet +``` +Repair-WinGetPackage [-Mode <PSPackageRepairMode>] [-Log <String>] [[-PSCatalogPackage] <PSCatalogPackage>] + [-Version <String>] [-ProgressAction <ActionPreference>] [-WhatIf] [-Confirm] [<CommonParameters>] +``` + +## DESCRIPTION +This command repairs a WinGet package from your computer, provided the package includes repair support. +The command includes parameters to specify values used to search for installed packages. By default, +all string-based searches are case-insensitive substring searches. Wildcards are not supported. +You can change the search behavior using the **MatchOption** parameter. + +> **Note: Not all packages support repair.** + +## EXAMPLES + +### Example 1: Repair a package using a query +```powershell +Repair-WinGetPackage -Query "Microsoft.GDK.2406" +``` +This example shows how to repair a package using a query. The **Query** parameter is positional, so you +don't need to include the parameter name before the query string. + +### Example 3 : Repair a package by Id +```powershell +Repair-WinGetPackage -Id "Microsoft.GDK.2406" +``` +This example shows how to repair a package by specifying the package identifier. + +If the package identifier is available from more than one source, you must provide additional search +criteria to select a specific instance of the package. + +### Example 3: Repair a package using by Name +```powershell +Repair-WinGetPackage -Name "Microsoft Game Development Kit - 240602 (June 2024 Update 2)" +``` +This example shows how to repair a package using the package name. + +> **Note: Please note that the examples mentioned above are mainly reference examples for the repair cmdlet and may not be operational as is, since many installers don't support repair as a standard functionality. For the Microsoft.GDK.2406 example, the assumption is that Microsoft.GDK.2406 supports repair capability and the author of the installer has provided the necessary repair context/switches in the Package Manifest in the Package Source referenced by the WinGet Client.** + +## PARAMETERS + +### -Id + +Specify the package identifier to search for. By default, the command does a case-insensitive +substring match. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Log + +Specify the location for the installer repair log. The value can be a fully-qualified or relative path and must include the file name. For example: `$env:TEMP\package.log`. + +> **Note: Not all installers support this option.** + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -MatchOption + +Specify the match option for a WinGet package query. This parameter accepts the following values: + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption +Parameter Sets: FoundSet +Aliases: +Accepted values: Equals, EqualsCaseInsensitive, StartsWithCaseInsensitive, ContainsCaseInsensitive + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Mode + +Specify the output mode for the installer. The parameter accepts the following values: + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackageRepairMode +Parameter Sets: (All) +Aliases: +Accepted values: Default, Silent, Interactive + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Moniker + +Specify the moniker of the WinGet package to repair. For example, the moniker for the +Microsoft.PowerShell package is `pwsh`. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Name + +Specify the name of the WinGet package name. The name contains space, you must enclose the name in +quotes. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PSCatalogPackage +Provide **PSCatalogPackage** object. You can get a **PSCatalogPackage** object by using the +`Find-WinGetPackage` or `Get-WingetPackage` commands. + + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage +Parameter Sets: GivenSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Query + +Specify one or more strings to search for. By default, the command searches all configured sources. +Wildcards are not supported. The command compares the value provided to the following package +manifest properties: + + - `PackageIdentifier` + - `PackageName` + - `Moniker` + - `Tags` + +The command does a case-insensitive substring comparison of these properties. + +```yaml +Type: System.String[] +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Version + +Specify the version of the package to be repaired. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.WinGet.Client.PSObjects.PSPackageRepairMode + +### Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage + +### System.String + +### System.String[] + +### Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSRepairResult + +## NOTES + +## RELATED LINKS + +[Find-WinGetPackage](Find-WinGetPackage.md) + +[Install-WinGetPackage](Install-WinGetPackage.md) + +[Uninstall-WinGetPackage](Uninstall-WinGetPackage.md) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackageRepairMode.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackageRepairMode.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSPackageRepairMode.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.PSObjects +{ + /// <summary> + /// Must match Microsoft.Management.Deployment.PackageRepairMode. + /// </summary> + public enum PSPackageRepairMode + { + /// <summary> + /// Default. + /// </summary> + Default, + + /// <summary> + /// Silent. + /// </summary> + Silent, + + /// <summary> + /// Interactive. + /// </summary> + Interactive, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairPackageCmdlet.cs @@ -0,0 +1,84 @@ +// ----------------------------------------------------------------------------- +// <copyright file="RepairPackageCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Client.PSObjects; + + /// <summary> + /// This class defines the repair package command. + /// </summary> + [Cmdlet( + VerbsDiagnostic.Repair, + Constants.WinGetNouns.Package, + DefaultParameterSetName = Constants.FoundSet, + SupportsShouldProcess = true)] + [OutputType(typeof(PSRepairResult))] + public sealed class RepairPackageCmdlet : PackageCmdlet + { + private RepairPackageCommand command = null; + + /// <summary> + /// Gets or sets the desired mode for the repair process. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public PSPackageRepairMode Mode { get; set; } = PSPackageRepairMode.Default; + + /// <summary> + /// Gets or sets the path to the logging file. + /// </summary> + [Parameter] + public string Log { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to skip the installer hash validation check. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AllowHashMismatch { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to continue upon non security related failures. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// <summary> + /// Repairs a package from the local system. + /// </summary> + protected override void ProcessRecord() + { + this.command = new RepairPackageCommand( + this, + this.AllowHashMismatch.ToBool(), + this.Force.ToBool(), + this.PSCatalogPackage, + this.Version, + this.Log, + this.Id, + this.Name, + this.Moniker, + this.Source, + this.Query); + this.command.Repair(this.MatchOption.ToString(), this.Mode.ToString()); + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/RepairPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/RepairPackageCommand.cs @@ -0,0 +1,157 @@ +// ----------------------------------------------------------------------------- +// <copyright file="RepairPackageCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Commands +{ + using System; + using System.Management.Automation; + using System.Threading.Tasks; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Exceptions; + using Microsoft.WinGet.Client.Engine.Helpers; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Resources; + + /// <summary> + /// This class defines the repair package command. + /// </summary> + public sealed class RepairPackageCommand : PackageCommand + { + /// <summary> + /// Initializes a new instance of the <see cref="RepairPackageCommand"/> class. + /// </summary> + /// <param name="psCmdlet">Caller cmdlet.</param> + /// <param name="psCatalogPackage">PSCatalogPackage.</param> + /// <param name="allowHashMismatch">To skip the installer hash validation check.</param> + /// <param name="force">To continue upon non security related failures.</param> + /// <param name="version">Version to repair.</param> + /// <param name="log">Logging file location.</param> + /// <param name="id">Package identifier.</param> + /// <param name="name">Name of package.</param> + /// <param name="moniker">Moniker of package.</param> + /// <param name="source">Source to search. if null, all are searched.</param> + /// <param name="query">Match against any field of a package.</param> + public RepairPackageCommand( + PSCmdlet psCmdlet, + bool allowHashMismatch, + bool force, + PSCatalogPackage psCatalogPackage, + string version, + string log, + string id, + string name, + string moniker, + string source, + string[] query) + : base(psCmdlet) + { + this.Force = force; + this.AllowHashMismatch = allowHashMismatch; + + if (psCatalogPackage != null) + { + this.CatalogPackage = psCatalogPackage; + } + + this.Version = version; + + this.Id = id; + this.Name = name; + this.Moniker = moniker; + this.Source = source; + this.Query = query; + + this.Log = log; + } + + /// <summary> + /// Gets or sets the path to the logging file. + /// </summary> + private string? Log { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to continue upon non security related failures. + /// </summary> + private bool Force { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to skip the installer hash validation check. + /// </summary> + private bool AllowHashMismatch { get; set; } + + /// <summary> + /// Process repair package. + /// </summary> + /// <param name="psPackageFieldMatchOption">PSPackageFieldMatchOption.</param> + /// <param name="psPackageRepairMode">PSPackageRepairMode.</param> + public void Repair( + string psPackageFieldMatchOption, + string psPackageRepairMode) + { + var result = this.Execute( + async () => await this.GetPackageAndExecuteAsync( + CompositeSearchBehavior.AllCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), + async (package, version) => + { + var repairOptions = this.GetRepairOptions(version, PSEnumHelpers.ToPackageRepairMode(psPackageRepairMode)); + return await this.RepairPackageAsync(package, repairOptions); + })); + + if (result != null) + { + if (result.Item1.Status == RepairResultStatus.RepairError + && result.Item1.ExtendedErrorCode != null) + { + if (result.Item1.ExtendedErrorCode.InnerException != null) + { + throw new WinGetRepairPackageException(result.Item1.ExtendedErrorCode.HResult, result.Item1.RepairerErrorCode, result.Item1.ExtendedErrorCode.InnerException); + } + + throw new WinGetRepairPackageException(result.Item1.ExtendedErrorCode.HResult, result.Item1.RepairerErrorCode); + } + + this.Write(WinGet.Common.Command.StreamType.Object, new PSRepairResult(result.Item1, result.Item2)); + } + } + + private RepairOptions GetRepairOptions( + PackageVersionId? version, + PackageRepairMode repairMode) + { + var options = ManagementDeploymentFactory.Instance.CreateRepairOptions(); + options.AllowHashMismatch = this.AllowHashMismatch; + options.Force = this.Force; + + if (this.Log != null) + { + options.LogOutputPath = this.Log; + } + + options.PackageRepairMode = repairMode; + + if (version != null) + { + options.PackageVersionId = version; + } + + return options; + } + + private async Task<RepairResult> RepairPackageAsync( + CatalogPackage catalogPackage, + RepairOptions repairOptions) + { + var progressOperation = new RepairOperationWithProgress( + this, + string.Format(Resources.ProgressRecordActivityRepairing, catalogPackage.Name)); + + return await progressOperation.ExecuteAsync( + () => PackageManagerWrapper.Instance.RepairPackageAsync(catalogPackage, repairOptions)); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/ErrorCode.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/ErrorCode.cs @@ -30,5 +30,30 @@ namespace Microsoft.WinGet.Client.Engine.Common /// Error code for ERROR_PACKAGE_NOT_REGISTERED_FOR_USER. /// </summary> public const int PackageNotRegisteredForUser = unchecked((int)0x80073D35); + + /// <summary> + /// Error code for APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND. + /// </summary> + public const int NoRepairInfoFound = unchecked((int)0x8A150079); + + /// <summary> + /// Error code for APPINSTALLER_CLI_ERROR_REPAIR_NOT_APPLICABLE. + /// </summary> + public const int RepairNotApplicable = unchecked((int)0x8A15007A); + + /// <summary> + /// Error code for APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED . + /// </summary> + public const int RepairerFailure = unchecked((int)0x8A15007B); + + /// <summary> + /// Error code for APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED. + /// </summary> + public const int RepairNotSupported = unchecked((int)0x8A15007C); + + /// <summary> + /// Error code for APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED. + /// </summary> + public const int AdminContextRepairProhibited = unchecked((int)0x8A15007D); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetRepairPackageException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetRepairPackageException.cs @@ -0,0 +1,79 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinGetRepairPackageException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Exceptions +{ + using System; + using System.Management.Automation; + using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Resources; + + /// <summary> + /// WinGetRepairPackageException. + /// </summary> + [Serializable] + public class WinGetRepairPackageException : RuntimeException + { + /// <summary> + /// Initializes a new instance of the <see cref="WinGetRepairPackageException"/> class. + /// </summary> + /// <param name="hresult">Repair operation ExtendedErrorCode Hresult.</param> + public WinGetRepairPackageException(int hresult) + : base(GetMessage(hresult)) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WinGetRepairPackageException"/> class. + /// </summary> + /// <param name="hresult">Repair operation ExtendedErrorCode Hresult.</param> + /// <param name="repairerExitCode">Repairer exit code.</param> + public WinGetRepairPackageException(int hresult, uint repairerExitCode) + : base(GetMessage(hresult, repairerExitCode)) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WinGetRepairPackageException"/> class. + /// </summary> + /// <param name="hresult">Repair operation ExtendedErrorCode Hresult.</param> + /// <param name="innerException">InnerException.</param> + public WinGetRepairPackageException(int hresult, Exception innerException) + : base(GetMessage(hresult), innerException) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WinGetRepairPackageException"/> class. + /// </summary> + /// <param name="hresult">Repair operation ExtendedErrorCode Hresult.</param> + /// <param name="repairerExitCode">Repairer exit code.</param> + /// <param name="innerException">InnerException.</param> + public WinGetRepairPackageException(int hresult, uint repairerExitCode, Exception innerException) + : base(GetMessage(hresult, repairerExitCode), innerException) + { + } + + private static string GetMessage(int hresult, uint repairerExitCode = 0) + { + switch (hresult) + { + case ErrorCode.NoRepairInfoFound: + return Resources.NoRepairInfoFound; + case ErrorCode.RepairerFailure: + return string.Format(Resources.RepairerFailure, repairerExitCode); + case ErrorCode.RepairNotSupported: + return Resources.RepairOperationNotSupported; + case ErrorCode.RepairNotApplicable: + return Resources.RepairDifferentInstallTechnology; + case ErrorCode.AdminContextRepairProhibited: + return Resources.NoAdminRepairForUserScopePackage; + default: + return string.Format(Resources.UnknownRepairFailure, hresult); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs @@ -30,6 +30,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Guid UninstallOptionsClsid = Guid.Parse("E1D9A11E-9F85-4D87-9C17-2B93143ADB8D"); private static readonly Guid PackageMatchFilterClsid = Guid.Parse("D02C9DAF-99DC-429C-B503-4E504E4AB000"); private static readonly Guid DownloadOptionsClsid = Guid.Parse("4CBABE76-7322-4BE4-9CEA-2589A80682DC"); + private static readonly Guid RepairOptionsClsid = Guid.Parse("0498F441-3097-455F-9CAF-148F28293865"); #else private static readonly Guid PackageManagerClsid = Guid.Parse("74CB3139-B7C5-4B9E-9388-E6616DEA288C"); private static readonly Guid FindPackagesOptionsClsid = Guid.Parse("1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96"); @@ -38,6 +39,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Guid UninstallOptionsClsid = Guid.Parse("AA2A5C04-1AD9-46C4-B74F-6B334AD7EB8C"); private static readonly Guid PackageMatchFilterClsid = Guid.Parse("3F85B9F4-487A-4C48-9035-2903F8A6D9E8"); private static readonly Guid DownloadOptionsClsid = Guid.Parse("8EF324ED-367C-4880-83E5-BB2ABD0B72F6"); + private static readonly Guid RepairOptionsClsid = Guid.Parse("E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03"); #endif [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type? PackageManagerType = Type.GetTypeFromCLSID(PackageManagerClsid); @@ -53,7 +55,11 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Type? PackageMatchFilterType = Type.GetTypeFromCLSID(PackageMatchFilterClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type? DownloadOptionsType = Type.GetTypeFromCLSID(DownloadOptionsClsid); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] + private static readonly Type? RepairOptionsType = Type.GetTypeFromCLSID(RepairOptionsClsid); + // These GUIDs correspond to the CSWinRT interface IIDs generated for Microsoft.Management.Deployment.Projection + // and are auto-generated by the WinRT tool. private static readonly Guid PackageManagerIid = Guid.Parse("B375E3B9-F2E0-5C93-87A7-B67497F7E593"); private static readonly Guid FindPackagesOptionsIid = Guid.Parse("A5270EDD-7DA7-57A3-BACE-F2593553561F"); private static readonly Guid CreateCompositePackageCatalogOptionsIid = Guid.Parse("21ABAA76-089D-51C5-A745-C85EEFE70116"); @@ -61,6 +67,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Guid UninstallOptionsIid = Guid.Parse("3EBC67F0-8339-594B-8A42-F90B69D02BBE"); private static readonly Guid PackageMatchFilterIid = Guid.Parse("D981ECA3-4DE5-5AD7-967A-698C7D60FC3B"); private static readonly Guid DownloadOptionsIid = Guid.Parse("94C92C4B-43F5-5CA3-BBBE-9F432C9546BC"); + private static readonly Guid RepairOptionsIid = Guid.Parse("263F0546-2D7E-53A0-B8D1-75B74817FF18"); private static readonly IEnumerable<Architecture> ValidArchs = new Architecture[] { Architecture.X86, Architecture.X64 }; @@ -153,6 +160,15 @@ namespace Microsoft.WinGet.Client.Engine.Helpers return Create<PackageMatchFilter>(PackageMatchFilterType, PackageMatchFilterIid); } + /// <summary> + /// Creates an instance of the <see cref="RepairOptions" /> class. + /// </summary> + /// <returns>A <see cref="RepairOptions" /> instance.</returns> + public RepairOptions CreateRepairOptions() + { + return Create<RepairOptions>(RepairOptionsType, RepairOptionsIid); + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static T Create<T>(Type? type, in Guid iid) where T : new() diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="PSEnumHelpers.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { using System; using Microsoft.Management.Deployment; + using Newtonsoft.Json.Linq; using Windows.System; /// <summary> @@ -132,5 +133,21 @@ namespace Microsoft.WinGet.Client.Engine.Helpers _ => throw new InvalidOperationException(), }; } + + /// <summary> + /// Converts PSPackageRepairMode string value to PackageRepairMode. + /// </summary> + /// <param name="value">PSPackageRepairMode string value.</param> + /// <returns>PackageRepairMode.</returns> + public static PackageRepairMode ToPackageRepairMode(string value) + { + return value switch + { + "Default" => PackageRepairMode.Default, + "Silent" => PackageRepairMode.Silent, + "Interactive" => PackageRepairMode.Interactive, + _ => throw new InvalidOperationException(), + }; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="PackageManagerWrapper.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -89,6 +89,19 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } /// <summary> + /// Wrapper for RepairPackagesAsync. + /// </summary> + /// <param name="package">The package to repair.</param> + /// <param name="options">The repair options.</param> + /// <returns>An async operation with progress.</returns> + public IAsyncOperationWithProgress<RepairResult, RepairProgress> RepairPackageAsync(CatalogPackage package, RepairOptions options) + { + return this.Execute( + () => this.packageManager.RepairPackageAsync(package, options), + false); + } + + /// <summary> /// Wrapper for GetPackageCatalogs. /// </summary> /// <returns>A list of PackageCatalogReferences.</returns> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/RepairOperationWithProgress.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/RepairOperationWithProgress.cs @@ -0,0 +1,44 @@ +// ----------------------------------------------------------------------------- +// <copyright file="RepairOperationWithProgress.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.Management.Automation; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; + using Windows.Foundation; + + /// <summary> + /// Handler for Repair Operation with Progress. + /// </summary> + internal class RepairOperationWithProgress : OperationWithProgressBase<RepairResult, RepairProgress> + { + /// <summary> + /// Initializes a new instance of the <see cref="RepairOperationWithProgress"/> class. + /// </summary> + /// <param name="pwshCmdlet"> instance.</param> + /// <param name="activity">Activity.</param> + public RepairOperationWithProgress(PowerShellCmdlet pwshCmdlet, string activity) + : base(pwshCmdlet, activity) + { + } + + /// <inheritdoc/> + public override void Progress(IAsyncOperationWithProgress<RepairResult, RepairProgress> operation, RepairProgress progress) + { + ProgressRecord record = new (this.ActivityId, this.Activity, progress.State.ToString()) + { + RecordType = ProgressRecordType.Processing, + }; + + record.StatusDescription = Resources.Repairing; + record.PercentComplete = (int)(progress.RepairCompletionProgress * 100); + this.PwshCmdlet.Write(StreamType.Progress, record); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSRepairResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSRepairResult.cs @@ -0,0 +1,137 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSRepairResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.PSObjects +{ + using System; + using Microsoft.Management.Deployment; + + /// <summary> + /// PSRepairResult. + /// </summary> + public sealed class PSRepairResult + { + private readonly RepairResult repairResult; + private readonly CatalogPackage catalogPackage; + + /// <summary> + /// Initializes a new instance of the <see cref="PSRepairResult"/> class. + /// </summary> + /// <param name="repairResult">The Repair result COM Object.</param> + /// <param name="catalogPackage">The catalog package COM Object.</param> + public PSRepairResult(RepairResult repairResult, CatalogPackage catalogPackage) + { + this.repairResult = repairResult; + this.catalogPackage = catalogPackage; + } + + /// <summary> + /// Gets the id of the repaired package. + /// </summary> + public string Id + { + get + { + return this.catalogPackage.Id; + } + } + + /// <summary> + /// Gets the name of the repaired package. + /// </summary> + public string Name + { + get + { + return this.catalogPackage.Name; + } + } + + /// <summary> + /// Gets the source name of the repaired package. + /// </summary> + public string Source + { + get + { + return this.catalogPackage.DefaultInstallVersion.PackageCatalog.Info.Name; + } + } + + /// <summary> + /// Gets the correlation data of the repair result. + /// </summary> + public string CorrelationData + { + get + { + return this.repairResult.CorrelationData; + } + } + + /// <summary> + /// Gets the extended error code exception of the failed repair result. + /// </summary> + public Exception ExtendedErrorCode + { + get + { + return this.repairResult.ExtendedErrorCode; + } + } + + /// <summary> + /// Gets a value indicating whether a reboot is required. + /// </summary> + public bool RebootRequired + { + get + { + return this.repairResult.RebootRequired; + } + } + + /// <summary> + /// Gets the error code of a repair. + /// </summary> + public uint RepairErrorCode + { + get + { + return this.repairResult.RepairerErrorCode; + } + } + + /// <summary> + /// Gets the status of the repair result. + /// </summary> + public string Status + { + get + { + return this.repairResult.Status.ToString(); + } + } + + /// <summary> + /// If the repair succeeded. + /// </summary> + /// <returns>True if repair succeeded.</returns> + public bool Succeeded() + { + return this.repairResult.Status == RepairResultStatus.Ok; + } + + /// <summary> + /// Message with error information. + /// </summary> + /// <returns>Error message.</returns> + public string ErrorMessage() + { + return $"RepairStatus : '{this.Status}' RepairErrorCode: '{this.RepairErrorCode}' ExtendedError: '{this.ExtendedErrorCode.HResult}'"; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs @@ -214,6 +214,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to Repair operations involving administrator privileges are not permitted on packages installed within the user scope.. + /// </summary> + internal static string NoAdminRepairForUserScopePackage { + get { + return ResourceManager.GetString("NoAdminRepairForUserScopePackage", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to No packages matched the given input criteria.. /// </summary> internal static string NoPackageFoundExceptionMessage { @@ -223,6 +232,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to The repair command for this package is not available in the Package Manifest. Please reach out to the package publisher for assistance.. + /// </summary> + internal static string NoRepairInfoFound { + get { + return ResourceManager.GetString("NoRepairInfoFound", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Exporting '{0}'. /// </summary> internal static string ProgressRecordActivityExporting { @@ -241,6 +259,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to Repairing '{0}'. + /// </summary> + internal static string ProgressRecordActivityRepairing { + get { + return ResourceManager.GetString("ProgressRecordActivityRepairing", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Uninstalling '{0}'. /// </summary> internal static string ProgressRecordActivityUninstalling { @@ -295,6 +322,24 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to The installer technology in use does not match the version currently installed.. + /// </summary> + internal static string RepairDifferentInstallTechnology { + get { + return ResourceManager.GetString("RepairDifferentInstallTechnology", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The repair operation was unsuccessful, exiting with Repairer error code: {0}.. + /// </summary> + internal static string RepairerFailure { + get { + return ResourceManager.GetString("RepairerFailure", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Failed to repair winget.. /// </summary> internal static string RepairFailureMessage { @@ -304,6 +349,24 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to Repairing. + /// </summary> + internal static string Repairing { + get { + return ResourceManager.GetString("Repairing", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The current installer technology does not support repair. Please reach out to the package vendor for assistance.. + /// </summary> + internal static string RepairOperationNotSupported { + get { + return ResourceManager.GetString("RepairOperationNotSupported", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to This cmdlet requires administrator privileges to execute.. /// </summary> internal static string RequiresAdminMessage { @@ -331,6 +394,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to An unexpected error happened while trying to repair the package. Error code:{0}. + /// </summary> + internal static string UnknownRepairFailure { + get { + return ResourceManager.GetString("UnknownRepairFailure", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to User settings file is invalid.. /// </summary> internal static string UserSettingsReadException { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <root> <!-- Microsoft ResX Schema @@ -237,4 +237,31 @@ <value>Exporting '{0}'</value> <comment>{Locked="{0}"} {0} - The name of the package being exported.</comment> </data> -</root>- \ No newline at end of file + <data name="ProgressRecordActivityRepairing" xml:space="preserve"> + <value>Repairing '{0}'</value> + <comment>{Locked="{0}"} {0} - The name of the package being repaired.</comment> + </data> + <data name="Repairing" xml:space="preserve"> + <value>Repairing</value> + </data> + <data name="NoRepairInfoFound" xml:space="preserve"> + <value>The repair command for this package is not available in the Package Manifest. Please reach out to the package publisher for assistance.</value> + </data> + <data name="RepairDifferentInstallTechnology" xml:space="preserve"> + <value>The installer technology in use does not match the version currently installed.</value> + </data> + <data name="RepairerFailure" xml:space="preserve"> + <value>The repair operation was unsuccessful, exiting with Repairer error code: {0}.</value> + <comment>{Locked="{0}"} {0} - The error code of the repairer.</comment> + </data> + <data name="RepairOperationNotSupported" xml:space="preserve"> + <value>The current installer technology does not support repair. Please reach out to the package vendor for assistance.</value> + </data> + <data name="NoAdminRepairForUserScopePackage" xml:space="preserve"> + <value>Repair operations involving administrator privileges are not permitted on packages installed within the user scope.</value> + </data> + <data name="UnknownRepairFailure" xml:space="preserve"> + <value>An unexpected error happened while trying to repair the package. Error code:{0}</value> + <comment>{Locked="{0}"} {0} - The error code from the repair operation.</comment> + </data> +</root> diff --git a/src/PowerShell/Microsoft.WinGet.Client/Examples/Sample_RepairPackage.ps1 b/src/PowerShell/Microsoft.WinGet.Client/Examples/Sample_RepairPackage.ps1 @@ -0,0 +1,22 @@ +<# + .SYNOPSIS + Example for 'Repair-WinGetPackage' cmdlet. + Repairs the specified application. +#> + +# TODO: Replace parameter with actual module name from PSGallery once module is released. +Param ( + [Parameter(Mandatory)] + $ModulePath +) + +Import-Module -Name $ModulePath + +# Repair a package by name +Repair-WinGetPackage -Name "PowerToys FileLocksmith Context Menu" + +# Repair a package by version and package identifier. +Repair-WinGetPackage -Id "MSIX\Microsoft.PowerToys.FileLocksmithContextMenu_1.0.0.0_neutral__8wekyb3d8bbwe" + +# Repair a package from a specific source +Repair-WinGetPackage -Id "MSIX\Microsoft.PowerToys.FileLocksmithContextMenu_1.0.0.0_neutral__8wekyb3d8bbwe" -Source winget diff --git a/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Format.ps1xml b/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Format.ps1xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="UTF-8" ?> +<?xml version="1.0" encoding="UTF-8" ?> <Configuration> <ViewDefinitions> <View> @@ -359,11 +359,75 @@ </TableColumnItem> <TableColumnItem> <ScriptBlock>$_.ProductCodes</ScriptBlock> - </TableColumnItem> + </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> + </View> + <View> + <Name>Microsoft.WinGet.Client.Engine.PSObjects.PSRepairResult</Name> + <ViewSelectedBy> + <TypeName>Microsoft.WinGet.Client.Engine.PSObjects.PSRepairResult</TypeName> + </ViewSelectedBy> + <TableControl> + <TableHeaders> + <TableColumnHeader> + <Label>Id</Label> + </TableColumnHeader> + <TableColumnHeader> + <Label>Name</Label> + </TableColumnHeader> + <TableColumnHeader> + <Label>Source</Label> + </TableColumnHeader> + <TableColumnHeader> + <Label>RepairErrorCode</Label> + </TableColumnHeader> + <TableColumnHeader> + <Label>Status</Label> + </TableColumnHeader> + <TableColumnHeader> + <Label>RebootRequired</Label> + </TableColumnHeader> + <TableColumnHeader> + <Label>ExtendedErrorCode</Label> + </TableColumnHeader> + <TableColumnHeader> + <Label>CorrelationData</Label> + </TableColumnHeader> + </TableHeaders> + <TableRowEntries> + <TableRowEntry> + <TableColumnItems> + <TableColumnItem> + <ScriptBlock>$_.Id</ScriptBlock> + </TableColumnItem> + <TableColumnItem> + <ScriptBlock>$_.Name</ScriptBlock> + </TableColumnItem> + <TableColumnItem> + <ScriptBlock>$_.Source</ScriptBlock> + </TableColumnItem> + <TableColumnItem> + <ScriptBlock>$_.RepairErrorCode</ScriptBlock> + </TableColumnItem> + <TableColumnItem> + <ScriptBlock>$_.Status</ScriptBlock> + </TableColumnItem> + <TableColumnItem> + <ScriptBlock>$_.RebootRequired</ScriptBlock> + </TableColumnItem> + <TableColumnItem> + <ScriptBlock>$_.ExtendedErrorCode</ScriptBlock> + </TableColumnItem> + <TableColumnItem> + <ScriptBlock>$_.CorrelationData</ScriptBlock> + </TableColumnItem> + </TableColumnItems> + </TableRowEntry> + </TableRowEntries> + </TableControl> </View> </ViewDefinitions> -</Configuration>- \ No newline at end of file +</Configuration> diff --git a/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 b/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 @@ -94,6 +94,7 @@ CmdletsToExport = @( 'Remove-WinGetSource' 'Reset-WinGetSource' 'Export-WinGetPackage' + 'Repair-WinGetPackage' ) # Variables to export from this module diff --git a/src/PowerShell/Microsoft.WinGet.Client/README.md b/src/PowerShell/Microsoft.WinGet.Client/README.md @@ -1,4 +1,4 @@ -# Windows Package Manager PowerShell Module +# Windows Package Manager PowerShell Module The Windows Package Manager PowerShell Module is made up on two components @@ -41,6 +41,7 @@ If the new cmdlet introduces a new dependency, please make sure to add it in the - Get-WinGetSetting - Remove-WinGetSource - Reset-WinGetSource +- Repair-WinGetPackage ## Quick Start Guide diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -123,6 +123,38 @@ BeforeAll { { return Join-Path -Path $env:Temp -ChildPath "WingetPwshTest-$(New-Guid)" } + + function Validate-WinGetResultCommonFields([psobject]$result, [psobject]$expected) { + $result | Should -Not -BeNullOrEmpty -ErrorAction Stop + $result.Id | Should -Be $expected.Id + $result.Name | Should -Be $expected.Name + $result.Source | Should -Be $expected.Source + $result.Status | Should -Be $expected.Status + } + + function Validate-WinGetPackageOperationResult([psobject]$result, [psobject]$expected, [string]$operationType) + { + Validate-WinGetResultCommonFields $result $expected + $result.RebootRequired | Should -Be $expected.RebootRequired + + switch ($operationType) { + 'install' { + $result.InstallerErrorCode | Should -Be $expected.InstallerErrorCode + } + 'update' { + $result.InstallerErrorCode | Should -Be $expected.InstallerErrorCode + } + 'repair' { + $result.RepairErrorCode | Should -Be $expected.RepairErrorCode + } + 'uninstall' { + $result.UninstallerErrorCode | Should -Be $expected.UninstallerErrorCode + } + default { + throw "Unknown operation type: $operationType" + } + } + } } Describe 'Get-WinGetVersion' { @@ -234,76 +266,56 @@ Describe 'Install|Update|Uninstall-WinGetPackage' { AddTestSource } + BeforeEach { + $expectedExeInstallerResult = [PSCustomObject]@{ + Id = "AppInstallerTest.TestExeInstaller" + Name = "TestExeInstaller" + Source = "TestSource" + Status = 'Ok' + RebootRequired = 'False' + InstallerErrorCode = 0 + UninstallerErrorCode = 0 + } + + $expectedPortableInstallerResult = [PSCustomObject]@{ + Id = "AppInstallerTest.TestPortableExe" + Name = "TestPortableExe" + Source = "TestSource" + Status = 'Ok' + RebootRequired = 'False' + InstallerErrorCode = 0 + UninstallerErrorCode = 0 + } + } + It 'Install by Id' { $result = Install-WinGetPackage -Id AppInstallerTest.TestExeInstaller -Version '1.0.0.0' - - $result | Should -Not -BeNullOrEmpty -ErrorAction Stop - $result.Id | Should -Be "AppInstallerTest.TestExeInstaller" - $result.Name | Should -Be "TestExeInstaller" - $result.Source | Should -Be "TestSource" - $result.InstallerErrorCode | Should -Be 0 - $result.Status | Should -Be 'Ok' - $result.RebootRequired | Should -Be 'False' + Validate-WinGetPackageOperationResult $result $expectedExeInstallerResult 'install' } It 'Install by exact Name and Version' { $result = Install-WinGetPackage -Name TestPortableExe -Version '2.0.0.0' -MatchOption Equals - - $result | Should -Not -BeNullOrEmpty -ErrorAction Stop - $result.Id | Should -Be "AppInstallerTest.TestPortableExe" - $result.Name | Should -Be "TestPortableExe" - $result.Source | Should -Be "TestSource" - $result.InstallerErrorCode | Should -Be 0 - $result.Status | Should -Be 'Ok' - $result.RebootRequired | Should -Be 'False' + Validate-WinGetPackageOperationResult $result $expectedPortableInstallerResult 'install' } It 'Update by Id' { $result = Update-WinGetPackage -Id AppInstallerTest.TestExeInstaller - - $result | Should -Not -BeNullOrEmpty -ErrorAction Stop - $result.Id | Should -Be "AppInstallerTest.TestExeInstaller" - $result.Name | Should -Be "TestExeInstaller" - $result.Source | Should -Be "TestSource" - $result.InstallerErrorCode | Should -Be 0 - $result.Status | Should -Be 'Ok' - $result.RebootRequired | Should -Be 'False' + Validate-WinGetPackageOperationResult $result $expectedExeInstallerResult 'update' } It 'Update by Name' { $result = Update-WinGetPackage -Name TestPortableExe - - $result | Should -Not -BeNullOrEmpty -ErrorAction Stop - $result.Id | Should -Be "AppInstallerTest.TestPortableExe" - $result.Name | Should -Be "TestPortableExe" - $result.Source | Should -Be "TestSource" - $result.InstallerErrorCode | Should -Be 0 - $result.Status | Should -Be 'Ok' - $result.RebootRequired | Should -Be 'False' + Validate-WinGetPackageOperationResult $result $expectedPortableInstallerResult 'update' } It 'Uninstall by Id' { $result = Uninstall-WinGetPackage -Id AppInstallerTest.TestExeInstaller - - $result | Should -Not -BeNullOrEmpty -ErrorAction Stop - $result.Id | Should -Be "AppInstallerTest.TestExeInstaller" - $result.Name | Should -Be "TestExeInstaller" - $result.Source | Should -Be "TestSource" - $result.UninstallerErrorCode | Should -Be 0 - $result.Status | Should -Be 'Ok' - $result.RebootRequired | Should -Be 'False' + Validate-WinGetPackageOperationResult $result $expectedExeInstallerResult 'uninstall' } It 'Uninstall by Name' { $result = Uninstall-WinGetPackage -Name TestPortableExe - - $result | Should -Not -BeNullOrEmpty -ErrorAction Stop - $result.Id | Should -Be "AppInstallerTest.TestPortableExe" - $result.Name | Should -Be "TestPortableExe" - $result.Source | Should -Be "TestSource" - $result.UninstallerErrorCode | Should -Be 0 - $result.Status | Should -Be 'Ok' - $result.RebootRequired | Should -Be 'False' + Validate-WinGetPackageOperationResult $result $expectedPortableInstallerResult 'uninstall' } AfterAll { @@ -319,7 +331,161 @@ Describe 'Install|Update|Uninstall-WinGetPackage' { { Uninstall-WinGetPackage -Id AppInstallerTest.TestPortableExe } - } + } +} + +Describe 'Install|Repair|Uninstall-WinGetPackage' { + + BeforeAll { + AddTestSource + } + + Context 'MSIX Repair Scenario' { + BeforeEach { + $expectedResult = [PSCustomObject]@{ + Id = "AppInstallerTest.TestMsixInstaller" + Name = "TestMsixInstaller" + Source = "TestSource" + Status = 'Ok' + RebootRequired = 'False' + InstallerErrorCode = 0 + RepairErrorCode = 0 + UninstallerErrorCode = 0 + } + } + + It 'Install MSIX By Id' { + $result = Install-WinGetPackage -Id AppInstallerTest.TestMsixInstaller + Validate-WinGetPackageOperationResult $result $expectedResult 'install' + } + + It 'Repair MSIX By Id' { + $result = Repair-WinGetPackage -Id AppInstallerTest.TestMsixInstaller + Validate-WinGetPackageOperationResult $result $expectedResult 'repair' + } + + It 'Uninstall MSIX By Id' { + $result = Uninstall-WinGetPackage -Id AppInstallerTest.TestMsixInstaller + Validate-WinGetPackageOperationResult $result $expectedResult 'uninstall' + } + } + + Context 'Burn installer "Modify" Repair Scenario' { + BeforeEach { + $expectedResult = [PSCustomObject]@{ + Id = "AppInstallerTest.TestModifyRepair" + Name = "TestModifyRepair" + Source = "TestSource" + Status = 'Ok' + RebootRequired = 'False' + InstallerErrorCode = 0 + RepairErrorCode = 0 + UninstallerErrorCode = 0 + } + } + + It 'Install Burn Installer By Id' { + $result = Install-WinGetPackage -Id AppInstallerTest.TestModifyRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'install' + } + + It 'Repair Burn Installer By Id' { + $result = Repair-WinGetPackage -Id AppInstallerTest.TestModifyRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'repair' + } + + It 'Uninstall Burn Installer By Id' { + $result = Uninstall-WinGetPackage -Id AppInstallerTest.TestModifyRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'uninstall' + } + } + + Context 'Exe Installer "Uninstaller" Repair Scenario' { + BeforeEach { + $expectedResult = [PSCustomObject]@{ + Id = "AppInstallerTest.UninstallerRepair" + Name = "UninstallerRepair" + Source = "TestSource" + Status = 'Ok' + RebootRequired = 'False' + InstallerErrorCode = 0 + RepairErrorCode = 0 + UninstallerErrorCode = 0 + } + } + + It 'Install Exe Installer By Id' { + $result = Install-WinGetPackage -Id AppInstallerTest.UninstallerRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'install' + } + + It 'Uninstaller Repair Exe Installer By Id' { + $result = Repair-WinGetPackage -Id AppInstallerTest.UninstallerRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'repair' + } + + It "Uninstall Exe Installer By Id" { + $result = Uninstall-WinGetPackage -Id AppInstallerTest.UninstallerRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'uninstall' + } + } + + Context 'Inno "Installer" Repair Scenario' { + BeforeEach { + $expectedResult = [PSCustomObject]@{ + Id = "AppInstallerTest.TestInstallerRepair" + Name = "TestInstallerRepair" + Source = "TestSource" + Status = 'Ok' + RebootRequired = 'False' + InstallerErrorCode = 0 + RepairErrorCode = 0 + UninstallerErrorCode = 0 + } + } + + It 'Install Exe Installer By Id' { + $result = Install-WinGetPackage -Id AppInstallerTest.TestInstallerRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'install' + } + + It 'Installer Repair Exe Installer By Id' { + $result = Repair-WinGetPackage -Id AppInstallerTest.TestInstallerRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'repair' + } + + It "Uninstall Exe Installer By Id" { + $result = Uninstall-WinGetPackage -Id AppInstallerTest.TestInstallerRepair + Validate-WinGetPackageOperationResult $result $expectedResult 'uninstall' + } + } + + AfterAll { + # Uninstall all test packages after each for proper cleanup. + $testMsix = Get-WinGetPackage -Id AppInstallerTest.TestMsixInstaller + if ($testMsix.Count -gt 0) + { + Uninstall-WinGetPackage -Id AppInstallerTest.TestMsixInstaller + } + + $testBurn = Get-WinGetPackage -Id AppInstallerTest.TestModifyRepair + if ($testBurn.Count -gt 0) + { + Uninstall-WinGetPackage -Id AppInstallerTest.TestModifyRepair + } + + $testExe = Get-WinGetPackage -Id AppInstallerTest.UninstallerRepair + if ($testExe.Count -gt 0) + { + Uninstall-WinGetPackage -Id AppInstallerTest.UninstallerRepair + } + + $testInno = Get-WinGetPackage -Id AppInstallerTest.TestInstallerRepair + if ($testInno.Count -gt 0) + { + Uninstall-WinGetPackage -Id AppInstallerTest.TestInstallerRepair + } + } } Describe 'Get-WinGetPackage' { diff --git a/src/WindowsPackageManager/main.cpp b/src/WindowsPackageManager/main.cpp @@ -29,6 +29,7 @@ CoCreatableClassWrlCreatorMapInclude(DownloadOptions); CoCreatableClassWrlCreatorMapInclude(PackageMatchFilter); CoCreatableClassWrlCreatorMapInclude(AuthenticationArguments); CoCreatableClassWrlCreatorMapInclude(PackageManagerSettings); +CoCreatableClassWrlCreatorMapInclude(RepairOptions); // Shim for configuration static functions CoCreatableClassWrlCreatorMapInclude(ConfigurationStaticFunctionsShim);