diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 1ecef0b368..3da65567e8 100644 --- 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 index 14b5b80a54..4af70ddc13 100644 --- 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 index 4ecedae490..cd30dc46b1 100644 --- 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 index 3b7ee3481e..2d1783c566 100644 --- 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 OrchestratorQueueItemFactory::CreateItemForRepair(std::wstring packageId, std::wstring sourceId, std::unique_ptr context) + { + std::unique_ptr item = std::make_unique(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 index 6a99ea3e61..2faed47e30 100644 --- 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 CreateItemForSearch(std::wstring packageId, std::wstring sourceId, std::unique_ptr context); // Create queue item for download static std::unique_ptr CreateItemForDownload(std::wstring packageId, std::wstring sourceId, std::unique_ptr context); + // Create queue item for repair + static std::unique_ptr CreateItemForRepair(std::wstring packageId, std::wstring sourceId, std::unique_ptr context); }; struct ContextOrchestrator diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index da1510292c..09ceb04d0f 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -60,6 +60,7 @@ public class Constants 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 index d4d215efa0..6b2e5ad826 100644 --- 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 @@ public void DisableWinGetPolicy() Assert.AreEqual(Constants.BlockByWinGetPolicyErrorMessage, groupPolicyException.Message); Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, groupPolicyException.HResult); + groupPolicyException = Assert.Catch(() => { 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 @@ public async Task DisableWinGetCommandLineInterfacesPolicy() 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 new file mode 100644 index 0000000000..7fc265dc18 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/Interop/RepairInterop.cs @@ -0,0 +1,356 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// Repair Interop Tests for COM/WinRT Interop classes. + /// + [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; + + /// + /// Initializes a new instance of the class. + /// + /// Initializer. + public RepairInterop(IInstanceInitializer initializer) + : base(initializer) + { + } + + /// + /// Test setup. + /// + [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); + } + + /// + /// Test Repair MSI Installer. + /// + /// A representing the asynchronous unit test. + [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); + } + } + + /// + /// Test Repair MSIX Installer. + /// + /// representing the asynchronous unit test. + [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()); + } + + /// + /// Test MSIX non-store package repair with machine scope. + /// + /// representing the asynchronous unit test. + [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); + } + + /// + /// Test repair of a Burn installer that has a "modify" repair behavior specified in the manifest. + /// + /// representing the asynchronous unit test. + [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")); + } + + /// + /// Tests the repair operation of a Burn installer that was installed in user scope but is being repaired in an admin context. + /// + /// representing the asynchronous unit test. + [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); + } + + /// + /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest and NoModify ARP flag set. + /// + /// representing the asynchronous unit test. + [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); + } + + /// + /// Tests the scenario where the repair operation is not supported for Portable Installer type. + /// + /// representing the asynchronous unit test. + [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); + } + + /// + /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest. + /// + /// representing the asynchronous unit test. + [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")); + } + + /// + /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest and NoRepair ARP flag set. + /// + /// representing the asynchronous unit test. + [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); + } + + /// + /// Test repair of a Nullsoft installer that has a "uninstaller" repair behavior specified in the manifest. + /// + /// representing the asynchronous unit test. + [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")); + } + + /// + /// Test repair of a Inno installer that has a "installer" repair behavior specified in the manifest. + /// + /// representing the asynchronous unit test. + [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 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 index 53c361ad72..11af2bd588 100644 --- a/src/AppInstallerCLIE2ETests/RepairCommand.cs +++ b/src/AppInstallerCLIE2ETests/RepairCommand.cs @@ -94,7 +94,7 @@ public void RepairNonStoreMsixPackageWithMachineScope() 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 index d490a7b4d1..45cb4314a0 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MissingRepairBehavior.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MissingRepairBehavior.yaml @@ -9,7 +9,6 @@ Installers: InstallerType: burn InstallerSha256: ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir 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 index 70b44ec797..b8b6ebae94 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepair.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: burn InstallerSha256: ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir 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 index 65a93af82f..751d8a4621 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepairWithNoModify.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepairWithNoModify.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: burn InstallerSha256: ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir 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 index 6bd501d5c1..1bad3082a4 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepair.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: exe InstallerSha256: ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir 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 index e816c82e04..e1c550b986 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepairWithNoRepair.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepairWithNoRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: exe InstallerSha256: ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir 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 index a43a34722b..e498ec6963 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestInnoInstaller.InstallerRepair.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestInnoInstaller.InstallerRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: inno InstallerSha256: ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir 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 index c1444e6626..659befcd52 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestNullsoftInstaller.UninstallerRepair.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestNullsoftInstaller.UninstallerRepair.yaml @@ -10,7 +10,6 @@ Installers: InstallerType: exe InstallerSha256: ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' - ElevationRequirement: elevationRequired InstallerSwitches: InstallLocation: /InstallDir Custom: /Version 2.0.0.0 /DisplayName NullsoftUninstallerRepair /UseHKLM diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest index 18d9ca333e..6604fcee74 100644 --- a/src/AppInstallerCLIPackage/Package.appxmanifest +++ b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -78,6 +78,8 @@ + + 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 index 55778515a8..f9e41531c7 100644 --- 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"/> + - diff --git a/src/Microsoft.Management.Deployment.OutOfProc/Factory.cpp b/src/Microsoft.Management.Deployment.OutOfProc/Factory.cpp index a7d8c15c07..ad7c0d66ca 100644 --- 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 s_nameCLSIDPairs + constexpr std::array 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 index 49012a2e1e..bf39299cc8 100644 --- 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 @@ internal static class ClassesDefinition { [ClsidContext.InProc] = new Guid("80CF9D63-5505-4342-B9B4-BB87895CA8BB"), } + }, + + [typeof(RepairOptions)] = new () + { + ProjectedClassType = typeof(RepairOptions), + InterfaceType = typeof(IRepairOptions), + Clsids = new Dictionary() + { + [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 index 4e457fed2e..73b75d5c70 100644 --- 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 @@ public WinGetProjectionFactory(IInstanceInitializer instanceInitializer) public AuthenticationArguments CreateAuthenticationArguments() => InstanceInitializer.CreateInstance(); - public PackageManagerSettings CreatePackageManagerSettings() => InstanceInitializer.CreateInstance(); + public PackageManagerSettings CreatePackageManagerSettings() => InstanceInitializer.CreateInstance(); + + public RepairOptions CreateRepairOptions() => InstanceInitializer.CreateInstance(); } } diff --git a/src/Microsoft.Management.Deployment/ComClsids.cpp b/src/Microsoft.Management.Deployment/ComClsids.cpp index 7d6f6aefe2..d76c1daf10 100644 --- 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 index b17c3dd322..e016a95dd4 100644 --- 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 index e07f3dc70c..fa0993e3ae 100644 --- 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) \ { \ resultStatus = TStatus::_installResultStatus_; \ @@ -44,6 +45,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { \ resultStatus = TStatus::_downloadResultStatus_; \ } \ + else if constexpr (std::is_same_v) \ + { \ + resultStatus = TStatus::_repairResultStatus_; \ + } \ template 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 index 8047c96419..a1c3e41274 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -217,6 +217,8 @@ + + @@ -257,6 +259,8 @@ Create + + diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters index fac175dc11..128b48aeff 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters @@ -38,6 +38,8 @@ + + @@ -81,6 +83,8 @@ + + diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 20c5dfb18f..6e21cab2e1 100644 --- 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(executionStage, terminationHR); + auto repairResult = winrt::make_self>(); + repairResult->Initialize(repairResultStatus, terminationHR, repairError, correlationData, rebootRequired); + return *repairResult; + } template 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) + { + 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) \ { \ progressState = TState::_installState_; \ @@ -241,7 +253,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation else if constexpr (std::is_same_v) \ { \ progressState = TState::_uninstallState_; \ - } + } \ + else if constexpr (std::is_same_v) \ + { \ + progressState = TState::_repairState_; \ + } \ template std::optional 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 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 std::unique_ptr CreateContextFromOperationOptions( TOptions options, @@ -571,6 +641,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { PopulateContextFromDownloadOptions(context.get(), options); } + else if constexpr (std::is_same_v) + { + 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 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(package); + std::shared_ptr<::AppInstaller::Repository::ICompositePackage> internalPackage = catalogPackageImpl->GetRepositoryPackage(); + comContext->Add(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 winrt::Windows::Foundation::IAsyncOperationWithProgress 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) + { + 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 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(true, std::move(queueItem)); } +#define WINGET_RETURN_REPAIR_RESULT_HR_IF(hr, boolVal) { if(boolVal) { return GetEmptyAsynchronousResultForOperation(hr, correlationData); }} +#define WINGET_RETURN_REPAIR_RESULT_HR_IF_FAILED(hr) { WINGET_RETURN_REPAIR_RESULT_HR_IF(hr, FAILED(hr)) } + + winrt::Windows::Foundation::IAsyncOperationWithProgress 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( + 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 index 3b07342be3..0ce0b32560 100644 --- 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 GetDownloadProgress(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PackageCatalogInfo catalogInfo); + // Contract 11.0 + winrt::Windows::Foundation::IAsyncOperationWithProgress + 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 index 43ade7aa71..6fe1965922 100644 --- 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 GetDownloadProgress(CatalogPackage package, PackageCatalogInfo catalogInfo); } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] + { + // Repair the specified package + Windows.Foundation.IAsyncOperationWithProgress RepairPackageAsync(CatalogPackage package, RepairOptions options); + } } /// Global settings for PackageManager operations. @@ -1351,5 +1482,9 @@ namespace Microsoft.Management.Deployment interface Windows.Foundation.Collections.IVectorView; interface Windows.Foundation.Collections.IVector; interface Windows.Foundation.Collections.IVectorView; + interface Windows.Foundation.Collections.IVector; + interface Windows.Foundation.Collections.IVectorView; + interface Windows.Foundation.Collections.IVector; + interface Windows.Foundation.Collections.IVectorView; } } diff --git a/src/Microsoft.Management.Deployment/Public/ComClsids.h b/src/Microsoft.Management.Deployment/Public/ComClsids.h index d42097f871..8d787d635f 100644 --- 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 new file mode 100644 index 0000000000..e321c69483 --- /dev/null +++ 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 +// 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 new file mode 100644 index 0000000000..2794d1f231 --- /dev/null +++ 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(); + + 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 + { + }; +} +#endif diff --git a/src/Microsoft.Management.Deployment/RepairResult.cpp b/src/Microsoft.Management.Deployment/RepairResult.cpp new file mode 100644 index 0000000000..c36725528a --- /dev/null +++ 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 + +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 new file mode 100644 index 0000000000..de41b23579 --- /dev/null +++ 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() = 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 index c660a395a7..9765cc16fb 100644 --- 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 new file mode 100644 index 0000000000..2b62027f9d --- /dev/null +++ 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 ] [-Log ] [-Version ] [-Id ] + [-Name ] [-Moniker ] [-Source ] [[-Query] ] + [-MatchOption ] [-ProgressAction ] [-WhatIf] [-Confirm] + [] +``` + +### GivenSet +``` +Repair-WinGetPackage [-Mode ] [-Log ] [[-PSCatalogPackage] ] + [-Version ] [-ProgressAction ] [-WhatIf] [-Confirm] [] +``` + +## 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 new file mode 100644 index 0000000000..445378fa26 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackageRepairMode.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.PSObjects +{ + /// + /// Must match Microsoft.Management.Deployment.PackageRepairMode. + /// + public enum PSPackageRepairMode + { + /// + /// Default. + /// + Default, + + /// + /// Silent. + /// + Silent, + + /// + /// Interactive. + /// + Interactive, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairPackageCmdlet.cs new file mode 100644 index 0000000000..49e5bba479 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairPackageCmdlet.cs @@ -0,0 +1,84 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// This class defines the repair package command. + /// + [Cmdlet( + VerbsDiagnostic.Repair, + Constants.WinGetNouns.Package, + DefaultParameterSetName = Constants.FoundSet, + SupportsShouldProcess = true)] + [OutputType(typeof(PSRepairResult))] + public sealed class RepairPackageCmdlet : PackageCmdlet + { + private RepairPackageCommand command = null; + + /// + /// Gets or sets the desired mode for the repair process. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public PSPackageRepairMode Mode { get; set; } = PSPackageRepairMode.Default; + + /// + /// Gets or sets the path to the logging file. + /// + [Parameter] + public string Log { get; set; } + + /// + /// Gets or sets a value indicating whether to skip the installer hash validation check. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AllowHashMismatch { get; set; } + + /// + /// Gets or sets a value indicating whether to continue upon non security related failures. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// + /// Repairs a package from the local system. + /// + 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()); + } + + /// + /// Interrupts currently running code within the command. + /// + 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 new file mode 100644 index 0000000000..143906fa2c --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/RepairPackageCommand.cs @@ -0,0 +1,157 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// This class defines the repair package command. + /// + public sealed class RepairPackageCommand : PackageCommand + { + /// + /// Initializes a new instance of the class. + /// + /// Caller cmdlet. + /// PSCatalogPackage. + /// To skip the installer hash validation check. + /// To continue upon non security related failures. + /// Version to repair. + /// Logging file location. + /// Package identifier. + /// Name of package. + /// Moniker of package. + /// Source to search. if null, all are searched. + /// Match against any field of a package. + 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; + } + + /// + /// Gets or sets the path to the logging file. + /// + private string? Log { get; set; } + + /// + /// Gets or sets a value indicating whether to continue upon non security related failures. + /// + private bool Force { get; set; } + + /// + /// Gets or sets a value indicating whether to skip the installer hash validation check. + /// + private bool AllowHashMismatch { get; set; } + + /// + /// Process repair package. + /// + /// PSPackageFieldMatchOption. + /// PSPackageRepairMode. + 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 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 index ed9c83b98a..f5a927e49e 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/ErrorCode.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/ErrorCode.cs @@ -30,5 +30,30 @@ internal static class ErrorCode /// Error code for ERROR_PACKAGE_NOT_REGISTERED_FOR_USER. /// public const int PackageNotRegisteredForUser = unchecked((int)0x80073D35); + + /// + /// Error code for APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND. + /// + public const int NoRepairInfoFound = unchecked((int)0x8A150079); + + /// + /// Error code for APPINSTALLER_CLI_ERROR_REPAIR_NOT_APPLICABLE. + /// + public const int RepairNotApplicable = unchecked((int)0x8A15007A); + + /// + /// Error code for APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED . + /// + public const int RepairerFailure = unchecked((int)0x8A15007B); + + /// + /// Error code for APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED. + /// + public const int RepairNotSupported = unchecked((int)0x8A15007C); + + /// + /// Error code for APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED. + /// + 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 new file mode 100644 index 0000000000..76071d645c --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetRepairPackageException.cs @@ -0,0 +1,79 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Exceptions +{ + using System; + using System.Management.Automation; + using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Resources; + + /// + /// WinGetRepairPackageException. + /// + [Serializable] + public class WinGetRepairPackageException : RuntimeException + { + /// + /// Initializes a new instance of the class. + /// + /// Repair operation ExtendedErrorCode Hresult. + public WinGetRepairPackageException(int hresult) + : base(GetMessage(hresult)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Repair operation ExtendedErrorCode Hresult. + /// Repairer exit code. + public WinGetRepairPackageException(int hresult, uint repairerExitCode) + : base(GetMessage(hresult, repairerExitCode)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Repair operation ExtendedErrorCode Hresult. + /// InnerException. + public WinGetRepairPackageException(int hresult, Exception innerException) + : base(GetMessage(hresult), innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Repair operation ExtendedErrorCode Hresult. + /// Repairer exit code. + /// InnerException. + 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 index 5994ed7cf7..c6e92189aa 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs @@ -30,6 +30,7 @@ internal sealed class ManagementDeploymentFactory 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 @@ internal sealed class ManagementDeploymentFactory 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 @@ internal sealed class ManagementDeploymentFactory 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 @@ internal sealed class ManagementDeploymentFactory 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 ValidArchs = new Architecture[] { Architecture.X86, Architecture.X64 }; @@ -153,6 +160,15 @@ public PackageMatchFilter CreatePackageMatchFilter() return Create(PackageMatchFilterType, PackageMatchFilterIid); } + /// + /// Creates an instance of the class. + /// + /// A instance. + public RepairOptions CreateRepairOptions() + { + return Create(RepairOptionsType, RepairOptionsIid); + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static T Create(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 index 8a00417fd4..be963f4cf2 100644 --- 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 (c) Microsoft Corporation. Licensed under the MIT License. // @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { using System; using Microsoft.Management.Deployment; + using Newtonsoft.Json.Linq; using Windows.System; /// @@ -132,5 +133,21 @@ public static PackageInstallerType ToPackageInstallerType(string value) _ => throw new InvalidOperationException(), }; } + + /// + /// Converts PSPackageRepairMode string value to PackageRepairMode. + /// + /// PSPackageRepairMode string value. + /// PackageRepairMode. + 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 index 451369fb57..94770f7138 100644 --- 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 (c) Microsoft Corporation. Licensed under the MIT License. // @@ -88,6 +88,19 @@ public IAsyncOperationWithProgress Down false); } + /// + /// Wrapper for RepairPackagesAsync. + /// + /// The package to repair. + /// The repair options. + /// An async operation with progress. + public IAsyncOperationWithProgress RepairPackageAsync(CatalogPackage package, RepairOptions options) + { + return this.Execute( + () => this.packageManager.RepairPackageAsync(package, options), + false); + } + /// /// Wrapper for GetPackageCatalogs. /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/RepairOperationWithProgress.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/RepairOperationWithProgress.cs new file mode 100644 index 0000000000..d773dd1fa4 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/RepairOperationWithProgress.cs @@ -0,0 +1,44 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// Handler for Repair Operation with Progress. + /// + internal class RepairOperationWithProgress : OperationWithProgressBase + { + /// + /// Initializes a new instance of the class. + /// + /// instance. + /// Activity. + public RepairOperationWithProgress(PowerShellCmdlet pwshCmdlet, string activity) + : base(pwshCmdlet, activity) + { + } + + /// + public override void Progress(IAsyncOperationWithProgress 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 new file mode 100644 index 0000000000..332e59bf11 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSRepairResult.cs @@ -0,0 +1,137 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.PSObjects +{ + using System; + using Microsoft.Management.Deployment; + + /// + /// PSRepairResult. + /// + public sealed class PSRepairResult + { + private readonly RepairResult repairResult; + private readonly CatalogPackage catalogPackage; + + /// + /// Initializes a new instance of the class. + /// + /// The Repair result COM Object. + /// The catalog package COM Object. + public PSRepairResult(RepairResult repairResult, CatalogPackage catalogPackage) + { + this.repairResult = repairResult; + this.catalogPackage = catalogPackage; + } + + /// + /// Gets the id of the repaired package. + /// + public string Id + { + get + { + return this.catalogPackage.Id; + } + } + + /// + /// Gets the name of the repaired package. + /// + public string Name + { + get + { + return this.catalogPackage.Name; + } + } + + /// + /// Gets the source name of the repaired package. + /// + public string Source + { + get + { + return this.catalogPackage.DefaultInstallVersion.PackageCatalog.Info.Name; + } + } + + /// + /// Gets the correlation data of the repair result. + /// + public string CorrelationData + { + get + { + return this.repairResult.CorrelationData; + } + } + + /// + /// Gets the extended error code exception of the failed repair result. + /// + public Exception ExtendedErrorCode + { + get + { + return this.repairResult.ExtendedErrorCode; + } + } + + /// + /// Gets a value indicating whether a reboot is required. + /// + public bool RebootRequired + { + get + { + return this.repairResult.RebootRequired; + } + } + + /// + /// Gets the error code of a repair. + /// + public uint RepairErrorCode + { + get + { + return this.repairResult.RepairerErrorCode; + } + } + + /// + /// Gets the status of the repair result. + /// + public string Status + { + get + { + return this.repairResult.Status.ToString(); + } + } + + /// + /// If the repair succeeded. + /// + /// True if repair succeeded. + public bool Succeeded() + { + return this.repairResult.Status == RepairResultStatus.Ok; + } + + /// + /// Message with error information. + /// + /// Error message. + 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 index f04162a2db..060cb7b614 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs @@ -213,6 +213,15 @@ internal static string InvalidVersionExceptionMessage { } } + /// + /// Looks up a localized string similar to Repair operations involving administrator privileges are not permitted on packages installed within the user scope.. + /// + internal static string NoAdminRepairForUserScopePackage { + get { + return ResourceManager.GetString("NoAdminRepairForUserScopePackage", resourceCulture); + } + } + /// /// Looks up a localized string similar to No packages matched the given input criteria.. /// @@ -222,6 +231,15 @@ internal static string NoPackageFoundExceptionMessage { } } + /// + /// 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.. + /// + internal static string NoRepairInfoFound { + get { + return ResourceManager.GetString("NoRepairInfoFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Exporting '{0}'. /// @@ -240,6 +258,15 @@ internal static string ProgressRecordActivityInstalling { } } + /// + /// Looks up a localized string similar to Repairing '{0}'. + /// + internal static string ProgressRecordActivityRepairing { + get { + return ResourceManager.GetString("ProgressRecordActivityRepairing", resourceCulture); + } + } + /// /// Looks up a localized string similar to Uninstalling '{0}'. /// @@ -294,6 +321,24 @@ internal static string RepairAppExecutionAliasMessage { } } + /// + /// Looks up a localized string similar to The installer technology in use does not match the version currently installed.. + /// + internal static string RepairDifferentInstallTechnology { + get { + return ResourceManager.GetString("RepairDifferentInstallTechnology", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The repair operation was unsuccessful, exiting with Repairer error code: {0}.. + /// + internal static string RepairerFailure { + get { + return ResourceManager.GetString("RepairerFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to repair winget.. /// @@ -303,6 +348,24 @@ internal static string RepairFailureMessage { } } + /// + /// Looks up a localized string similar to Repairing. + /// + internal static string Repairing { + get { + return ResourceManager.GetString("Repairing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The current installer technology does not support repair. Please reach out to the package vendor for assistance.. + /// + internal static string RepairOperationNotSupported { + get { + return ResourceManager.GetString("RepairOperationNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to This cmdlet requires administrator privileges to execute.. /// @@ -330,6 +393,15 @@ internal static string Uninstalling { } } + /// + /// Looks up a localized string similar to An unexpected error happened while trying to repair the package. Error code:{0}. + /// + internal static string UnknownRepairFailure { + get { + return ResourceManager.GetString("UnknownRepairFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to User settings file is invalid.. /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx index 735c55e853..174ff2c4ab 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx @@ -1,4 +1,4 @@ - +