From ed277fb1f6734da9537ae1522e721a90b8e13157 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Mon, 3 Jun 2024 17:42:28 -0700 Subject: [PATCH 01/17] [WinGet][Repair] Winget Repair - Eliminate installer type mapping for MSI/WIX and MSIX NonStore & code refactoring The changes includes: - Code changes eliminate installer type mapping for MSI/WIX and MSIX NonStore, leveraging native platform support for repairs. - Refactored code for better readability and maintainability. --- .../Commands/RepairCommand.cpp | 16 +- .../Workflows/RepairFlow.cpp | 284 +++++++++++++----- .../Workflows/RepairFlow.h | 14 + 3 files changed, 224 insertions(+), 90 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/RepairCommand.cpp b/src/AppInstallerCLICore/Commands/RepairCommand.cpp index 210dabb584..ff992baafa 100644 --- a/src/AppInstallerCLICore/Commands/RepairCommand.cpp +++ b/src/AppInstallerCLICore/Commands/RepairCommand.cpp @@ -104,11 +104,7 @@ namespace AppInstaller::CLI Workflow::GetManifestFromArg << Workflow::ReportManifestIdentity << Workflow::SearchSourceUsingManifest << - Workflow::EnsureOneMatchFromSearchResult(OperationType::Repair) << - Workflow::GetInstalledPackageVersion << - Workflow::SelectInstaller << - Workflow::EnsureApplicableInstaller << - Workflow::RepairSinglePackage; + Workflow::EnsureOneMatchFromSearchResult(OperationType::Repair); } else { @@ -116,10 +112,12 @@ namespace AppInstaller::CLI Workflow::SearchSourceForSingle << Workflow::HandleSearchResultFailures << Workflow::EnsureOneMatchFromSearchResult(OperationType::Repair) << - Workflow::ReportPackageIdentity << - Workflow::GetInstalledPackageVersion << - Workflow::SelectApplicablePackageVersion << - Workflow::RepairSinglePackage; + Workflow::ReportPackageIdentity; } + + context << + Workflow::GetInstalledPackageVersion << + Workflow::SelectApplicableInstallerWhenEssential << + Workflow::RepairSinglePackage; } } diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index 44e05419d5..8f2ffb344a 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -19,12 +19,23 @@ using namespace AppInstaller::Manifest; using namespace AppInstaller::Msix; using namespace AppInstaller::Repository; +using namespace AppInstaller::CLI::Execution; namespace AppInstaller::CLI::Workflow { // Internal implementation details namespace { + // Handles Repair behavior Unknown scenario. + // RequiredArgs:None + // Inputs:None + // Outputs:None + void HandleUnknownRepair(Execution::Context& context) + { + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); + } + // Sets the uninstall string in the context. // RequiredArgs: // Inputs:InstalledPackageVersion @@ -49,8 +60,7 @@ namespace AppInstaller::CLI::Workflow if (uninstallCommandItr == packageMetadata.end()) { - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); + context << HandleUnknownRepair; } context.Add(uninstallCommandItr->second); @@ -69,8 +79,7 @@ namespace AppInstaller::CLI::Workflow auto modifyPathItr = packageMetadata.find(PackageVersionMetadata::StandardModifyCommand); if (modifyPathItr == packageMetadata.end()) { - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); + context << HandleUnknownRepair; } context.Add(modifyPathItr->second); @@ -87,8 +96,7 @@ namespace AppInstaller::CLI::Workflow if (productCodes.empty()) { - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); + context << HandleUnknownRepair; } context.Add(productCodes); @@ -105,13 +113,23 @@ namespace AppInstaller::CLI::Workflow auto packageFamilyNames = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); if (packageFamilyNames.empty()) { - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); + context << HandleUnknownRepair; } context.Add(packageFamilyNames); } + // Obtains the installer type from the installed package. + // RequiredArgs: None + // Inputs: InstalledPackageVersion + // Outputs: InstallerTypeEnum + InstallerTypeEnum GetInstalledPackageInstallerType(Execution::Context& context) + { + const auto& installedPackage = context.Get(); + std::string installedType = installedPackage->GetMetadata()[PackageVersionMetadata::InstalledType]; + return ConvertToInstallerTypeEnum(installedType); + } + // The function performs a preliminary check on the installed package by reading its ARP registry flags for NoModify and NoRepair to confirm if the repair operation is applicable. // RequiredArgs:None // Inputs:InstalledPackageVersion, NoModify ?, NoRepair ? @@ -151,6 +169,15 @@ namespace AppInstaller::CLI::Workflow // Outputs:None void ApplicabilityCheckForAvailablePackage(Execution::Context& context) { + InstallerTypeEnum installedPackageInstallerType = GetInstalledPackageInstallerType(context); + + // Skip the Available Package applicability check for MSI and MSIX repair as they aren't needed. + if (installedPackageInstallerType == InstallerTypeEnum::Msi + || installedPackageInstallerType == InstallerTypeEnum::Msix) + { + return; + } + // Selected Installer repair applicability check auto installerType = context.Get()->EffectiveInstallerType(); auto repairBehavior = context.Get()->RepairBehavior; @@ -165,11 +192,50 @@ namespace AppInstaller::CLI::Workflow if (DoesInstallerTypeRequireRepairBehaviorForRepair(installerType) && repairBehavior == RepairBehaviorEnum::Unknown) { - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); + context << HandleUnknownRepair; } } + // Sets the repair string in the context based on the repair behavior and installer type. + // RequiredArgs:None + // Inputs:Installer, RepairBehavior, InstalledPackageVersion, InstallerArgs + // Outputs:RepairString + void HandleModifyRepairBehavior(Execution::Context& context, std::string& repairCommand) + { + SetModifyPathInContext(context); + repairCommand += context.Get(); + } + + // Sets the repair string in the context based on the repair behavior and installer type. + // RequiredArgs:None + // Inputs:Installer, RepairBehavior, InstalledPackageVersion, InstallerArgs + // Outputs:RepairString + void HandleInstallerRepairBehavior(Execution::Context& context, InstallerTypeEnum installerType) + { + context << + ShowInstallationDisclaimer << + ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << + DownloadInstaller; + + if (installerType == InstallerTypeEnum::Zip) + { + context << + ScanArchiveFromLocalManifest << + ExtractFilesFromArchive << + VerifyAndSetNestedInstaller; + } + } + + // Sets the repair string in the context based on the repair behavior and installer type. + // RequiredArgs:None + // Inputs:Installer, RepairBehavior, InstalledPackageVersion, InstallerArgs + // Outputs:RepairString + void HandleUninstallerRepairBehavior(Execution::Context& context, std::string& repairCommand) + { + SetUninstallStringInContext(context); + repairCommand += context.Get(); + } + // Generate the repair string based on the repair behavior and installer type. // RequiredArgs:None // Inputs:BaseInstallerType, RepairBehavior, ModifyPath?, UninstallString?, InstallerArgs @@ -185,81 +251,100 @@ namespace AppInstaller::CLI::Workflow switch (repairBehavior) { case RepairBehaviorEnum::Modify: - { - SetModifyPathInContext(context); - repairCommand.append(context.Get()); - } - break; + HandleModifyRepairBehavior(context, repairCommand); + break; case RepairBehaviorEnum::Installer: - { - // [NOTE:] We will ShellExecuteInstall for this scenario which uses installer path directly.so no need for repair command generation. - // We prepare installer download and archive extraction here. - context << - ShowInstallationDisclaimer << - ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << - DownloadInstaller; - - if (installerType == InstallerTypeEnum::Zip) - { - context << - ScanArchiveFromLocalManifest << - ExtractFilesFromArchive << - VerifyAndSetNestedInstaller; - } - } - break; + HandleInstallerRepairBehavior(context, installerType); + break; case RepairBehaviorEnum::Uninstaller: - { - SetUninstallStringInContext(context); - repairCommand.append(context.Get()); - } - break; + HandleUninstallerRepairBehavior(context, repairCommand); + break; case RepairBehaviorEnum::Unknown: default: - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); + context << HandleUnknownRepair; } context << GetInstallerArgs; - // Following is not applicable for RepairBehaviorEnum::Installer, as we can call ShellExecuteInstall directly with repair argument. - if (repairBehavior != RepairBehaviorEnum::Installer) + // If the repair behavior is set to 'Installer', we can proceed with the repair command as is. + // For repair behaviors other than 'Installer', subsequent steps will be necessary to prepare the repair command. + if (repairBehavior == RepairBehaviorEnum::Installer) { - if (repairCommand.empty()) - { - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); - } + return; + } - repairCommand.append(" "); - repairCommand.append(context.Get()); - context.Add(repairCommand); + if (repairCommand.empty()) + { + context << HandleUnknownRepair; } + + repairCommand += " "; + repairCommand += context.Get(); + context.Add(repairCommand); } - } - void RunRepairForRepairBehaviorBasedInstaller(Execution::Context& context) - { - const auto& installer = context.Get(); - auto repairBehavior = installer->RepairBehavior; + // Determines if installer mapping is required for the given installed package. + // RequiredArgs: None + // Inputs: InstalledPackageVersion + // Outputs: None + bool IsInstallerMappingRequired(Execution::Context& context) + { + const auto& installedPackage = context.Get(); + std::string installedType = installedPackage->GetMetadata()[PackageVersionMetadata::InstalledType]; + InstallerTypeEnum installerTypeEnum = GetInstalledPackageInstallerType(context); - if (repairBehavior == RepairBehaviorEnum::Modify || repairBehavior == RepairBehaviorEnum::Uninstaller) + // Installer mapping is not required for MSI and MSIX (Non-store) repair, as we rely on platform support and its dependency on the installed package. + if (installerTypeEnum == InstallerTypeEnum::Msi + || (installerTypeEnum == InstallerTypeEnum::Msix && installedPackage->GetSource() != WellKnownSource::MicrosoftStore)) + { + return false; + } + else + { + return true; + } + } + + // Manages the repair operation for the installed package, specifically for the repair behavior types 'Modify' and 'Uninstall'. + // RequiredArgs: None + // Inputs: RepairBehavior + // Outputs: None + void HandleModifyOrUninstallerRepair(Execution::Context& context, RepairBehaviorEnum repairBehavior) { context << ShellExecuteRepairImpl << ReportRepairResult(RepairBehaviorToString(repairBehavior), APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED); } - else if (repairBehavior == RepairBehaviorEnum::Installer) + + // Manages the repair operation for the installed package, specifically for the repair behavior type 'Installer'. + // RequiredArgs: None + // Inputs: RepairBehavior + // Outputs: None + void HandleInstallerRepair(Execution::Context& context, RepairBehaviorEnum repairBehavior) { context << ShellExecuteInstallImpl << ReportInstallerResult(RepairBehaviorToString(repairBehavior), APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED); } - else + } + + void RunRepairForRepairBehaviorBasedInstaller(Execution::Context& context) + { + const auto& installer = context.Get(); + auto repairBehavior = installer->RepairBehavior; + + switch (repairBehavior) { - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); + case RepairBehaviorEnum::Modify: + case RepairBehaviorEnum::Uninstaller: + HandleModifyOrUninstallerRepair(context, repairBehavior); + break; + case RepairBehaviorEnum::Installer: + HandleInstallerRepair(context, repairBehavior); + break; + default: + HandleUnknownRepair(context); } } @@ -279,11 +364,7 @@ namespace AppInstaller::CLI::Workflow void ExecuteRepair(Execution::Context& context) { - // [TODO:] At present, the repair flow necessitates a mapped available installer. - // However, future refactoring should allow for msix/msi repair without the need for one. - - const auto& installer = context.Get(); - InstallerTypeEnum installerTypeEnum = installer->EffectiveInstallerType(); + InstallerTypeEnum installerTypeEnum = GetInstalledPackageInstallerType(context); Synchronization::CrossProcessInstallLock lock; @@ -303,8 +384,8 @@ namespace AppInstaller::CLI::Workflow switch (installerTypeEnum) { - case InstallerTypeEnum::Burn: case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: case InstallerTypeEnum::Inno: case InstallerTypeEnum::Nullsoft: { @@ -320,16 +401,10 @@ namespace AppInstaller::CLI::Workflow } break; case InstallerTypeEnum::Msix: - { - context << - RepairMsixPackage; - } - break; case InstallerTypeEnum::MSStore: { context << - EnsureStorePolicySatisfied << - MSStoreRepair; + RepairMsixPackage; } break; case InstallerTypeEnum::Portable: @@ -340,11 +415,11 @@ namespace AppInstaller::CLI::Workflow void GetRepairInfo(Execution::Context& context) { - const auto& installer = context.Get(); - InstallerTypeEnum installerTypeEnum = installer->EffectiveInstallerType(); + InstallerTypeEnum installerTypeEnum = GetInstalledPackageInstallerType(context); switch (installerTypeEnum) { + // Exe based installers, for installed package all gets mapped to exe extension. case InstallerTypeEnum::Burn: case InstallerTypeEnum::Exe: case InstallerTypeEnum::Inno: @@ -354,6 +429,7 @@ namespace AppInstaller::CLI::Workflow GenerateRepairString; } break; + // MSI based installers, for installed package all gets mapped to msi extension. case InstallerTypeEnum::Msi: case InstallerTypeEnum::Wix: { @@ -361,14 +437,14 @@ namespace AppInstaller::CLI::Workflow SetProductCodesInContext; } break; + // MSIX based installers, msix, msstore. case InstallerTypeEnum::Msix: + case InstallerTypeEnum::MSStore: { context << SetPackageFamilyNamesInContext; } break; - case InstallerTypeEnum::MSStore: - break; case InstallerTypeEnum::Portable: default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); @@ -376,6 +452,33 @@ namespace AppInstaller::CLI::Workflow } void RepairMsixPackage(Execution::Context& context) + { + const auto& installedPackage = context.Get(); + std::string installedType = installedPackage->GetMetadata()[PackageVersionMetadata::InstalledType]; + + if (ConvertToInstallerTypeEnum(installedType) == InstallerTypeEnum::Msix) + { + // If the installed package is from Microsoft Store, then we attempt to repair it using MSStoreRepair else do package re-registration. + if (installedPackage->GetSource() == WellKnownSource::MicrosoftStore) + { + context << + EnsureStorePolicySatisfied << + MSStoreRepair; + } + else + { + context << + RepairMsixNonStorePackage; + } + } + else + { + // This should never happen as the installer type should be one of the above. + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + + void RepairMsixNonStorePackage(Execution::Context& context) { bool isMachineScope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)) == Manifest::ScopeEnum::Machine; @@ -459,9 +562,26 @@ namespace AppInstaller::CLI::Workflow context << GetManifestWithVersionFromPackage( requestedVersion, - context.Args.GetArg(Execution::Args::Type::Channel), false) << - SelectInstaller << - EnsureApplicableInstaller; + context.Args.GetArg(Execution::Args::Type::Channel), false); + } + + void SelectApplicableInstallerWhenEssential(Execution::Context& context) + { + // For MSI installers, the platform provides built-in support for repair via msiexec, hence no need to select an installer. + // Similarly, for MSIX packages that are not from the Microsoft Store, selecting an installer is not required. + if (IsInstallerMappingRequired(context)) + { + // Non Manifest based repair flow + if (!context.Args.Contains(Args::Type::Manifest)) + { + context << + SelectApplicablePackageVersion; + } + + context << + SelectInstaller << + EnsureApplicableInstaller; + } } void ReportRepairResult::operator()(Execution::Context& context) const @@ -470,7 +590,9 @@ namespace AppInstaller::CLI::Workflow if (repairResult != 0) { - const auto& repairPackage = context.Get(); + auto& repairPackage = IsInstallerMappingRequired(context) ? + context.Get() : + context.Get(); Logging::Telemetry().LogRepairFailure( repairPackage->GetProperty(PackageVersionProperty::Id), @@ -505,4 +627,4 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::RepairFlowRepairSuccess << std::endl; } } -} \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.h b/src/AppInstallerCLICore/Workflows/RepairFlow.h index 6af77249df..0be3bb29c2 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.h +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include "ExecutionContext.h" +#include "winget/ManifestCommon.h" namespace AppInstaller::CLI::Workflow { @@ -41,12 +42,25 @@ namespace AppInstaller::CLI::Workflow // Outputs:None void RepairMsixPackage(Execution::Context& context); + // Perform the repair operation for the MSIX NonStore package. + // RequiredArgs:None + // Inputs:PackageFamilyNames , InstallScope? + // Outputs:None + void RepairMsixNonStorePackage(Execution::Context& context); + // Select the applicable package version by matching the installed package version with the available package version. // RequiredArgs:None // Inputs: Package,InstalledPackageVersion, AvailablePackageVersions // Outputs:Manifest, PackageVersion, Installer void SelectApplicablePackageVersion(Execution::Context& context); + /// + /// Select the applicable installer for the installed package is essential. + // RequiredArgs:None + // Inputs: Package,InstalledPackageVersion, AvailablePackageVersions + // Outputs:Manifest, PackageVersion, Installer + void SelectApplicableInstallerWhenEssential(Execution::Context& context); + // Perform the repair operation for the single package. // RequiredArgs:None // Inputs: SearchResult, InstalledPackage, ApplicableInstaller From 99bf2e4c4420b53d0915f57bed461b7e2eb68b74 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Mon, 3 Jun 2024 17:48:05 -0700 Subject: [PATCH 02/17] AppInstallerTestExeInstaller - Repair support - Refactored AppInstallerTestExeInstaller to support repair operations. The code now accommodates Modify Repair, Uninstaller Repair, and Installer Repair. - Minor refactoring is also part of this update --- src/AppInstallerTestExeInstaller/main.cpp | 187 ++++++++++++++++++++-- 1 file changed, 172 insertions(+), 15 deletions(-) diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp index aed19942bc..8df9447472 100644 --- a/src/AppInstallerTestExeInstaller/main.cpp +++ b/src/AppInstallerTestExeInstaller/main.cpp @@ -16,6 +16,42 @@ std::wstring_view DefaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; std::wstring_view DefaultDisplayName = L"AppInstallerTestExeInstaller"; std::wstring_view DefaultDisplayVersion = L"1.0.0.0"; +void WriteModifyRepairScript(std::wofstream& script, const path& repairCompletedTextFilePath, bool isModifyScript) { + std::wstring scriptName = isModifyScript ? L"Modify" : L"Uninstaller"; + script << L" if /I \"%%A\"==\"/repair\" (\n" + << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully > \"" << repairCompletedTextFilePath.wstring() << "\"\n" + << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully\n" + << L" EXIT /B 0\n" + << L" ) else if /I \"%%A\"==\"/r\" (\n" + << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully > \"" << repairCompletedTextFilePath.wstring() << "\"\n" + << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully\n" + << L" EXIT /B 0\n" + << L" )"; +} + +void WriteModifyUninstallScript(std::wofstream& script) { + script << L" else if /I \"%%A\"==\"/uninstall\" (\n" + << L" call UninstallTestExe.bat\n" + << L" EXIT /B 0\n" + << L" ) else if /I \"%%A\"==\"/X\" (\n" + << L" call UninstallTestExe.bat\n" + << L" EXIT /B 0\n" + << L" )\n"; +} + +void WriteModifyInvalidOperationScript(std::wofstream& script) { + script << L"echo Invalid operation\n" + << L"EXIT /B 1\n"; +} + +void WriteUninstallerScript(std::wofstream& uninstallerScript, const path& uninstallerOutputTextFilePath, const std::wstring& registryKey, const path& modifyScriptPath, const path& repairCompletedTextFilePath) { + uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; + uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; + uninstallerScript << "REG DELETE " << registryKey << " /f\n"; + uninstallerScript << "if exist \"" << modifyScriptPath.wstring() << "\" del \"" << modifyScriptPath.wstring() << "\"\n"; + uninstallerScript << "if exist \"" << repairCompletedTextFilePath.wstring() << "\" del \"" << repairCompletedTextFilePath.wstring() << "\"\n"; +} + path GenerateUninstaller(std::wostream& out, const path& installDirectory, const std::wstring& productID, bool useHKLM) { path uninstallerPath = installDirectory; @@ -26,6 +62,12 @@ path GenerateUninstaller(std::wostream& out, const path& installDirectory, const path uninstallerOutputTextFilePath = installDirectory; uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; + path repairCompletedTextFilePath = installDirectory; + repairCompletedTextFilePath /= "TestExeRepairCompleted.txt"; + + path modifyScriptPath = installDirectory; + modifyScriptPath /= "ModifyTestExe.bat"; + std::wstring registryKey{ useHKLM ? L"HKEY_LOCAL_MACHINE\\" : L"HKEY_CURRENT_USER\\" }; registryKey += RegistrySubkey; if (!productID.empty()) @@ -39,18 +81,43 @@ path GenerateUninstaller(std::wostream& out, const path& installDirectory, const std::wofstream uninstallerScript(uninstallerPath); uninstallerScript << "@echo off\n"; - uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; - uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; - uninstallerScript << "REG DELETE " << registryKey << " /f\n"; + uninstallerScript << L"for %%A in (%*) do (\n"; + WriteModifyRepairScript(uninstallerScript, repairCompletedTextFilePath, false /*isModifyScript*/); + uninstallerScript << ")\n"; + WriteUninstallerScript(uninstallerScript, uninstallerOutputTextFilePath, registryKey, modifyScriptPath, repairCompletedTextFilePath); + uninstallerScript.close(); return uninstallerPath; } +path GenerateModifyPath(const path& installDirectory) +{ + path modifyScriptPath = installDirectory; + modifyScriptPath /= "ModifyTestExe.bat"; + + path repairCompletedTextFilePath = installDirectory; + repairCompletedTextFilePath /= "TestExeRepairCompleted.txt"; + + std::wofstream modifyScript(modifyScriptPath); + + modifyScript << L"@echo off\n"; + modifyScript << L"for %%A in (%*) do (\n"; + WriteModifyRepairScript(modifyScript, repairCompletedTextFilePath, true /*isModifyScript*/); + WriteModifyUninstallScript(modifyScript); + modifyScript << L")\n"; + WriteModifyInvalidOperationScript(modifyScript); + + modifyScript.close(); + + return modifyScriptPath; +} + void WriteToUninstallRegistry( std::wostream& out, const std::wstring& productID, const path& uninstallerPath, + const path& modifyPath, const std::wstring& displayName, const std::wstring& displayVersion, const std::wstring& installLocation, @@ -62,16 +129,18 @@ void WriteToUninstallRegistry( // String inputs to registry must be of wide char type const wchar_t* publisher = L"Microsoft Corporation"; std::wstring uninstallString = uninstallerPath.wstring(); + std::wstring modifyPathString = modifyPath.wstring(); + DWORD version = 1; std::wstring registryKey{ RegistrySubkey }; - if (!productID.empty()) + if (!productID.empty()) { registryKey += productID; out << "Product Code overridden to: " << registryKey << std::endl; } - else + else { registryKey += DefaultProductID; out << "Default Product Code used: " << registryKey << std::endl; @@ -128,6 +197,12 @@ void WriteToUninstallRegistry( out << "Failed to write InstallLocation value. Error Code: " << res << std::endl; } + // Set ModifyPath Property Value + if (LONG res = RegSetValueEx(hkey, L"ModifyPath", NULL, REG_EXPAND_SZ, (LPBYTE)modifyPath.c_str(), (DWORD)(modifyPath.wstring().length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + out << "Failed to write ModifyPath value. Error Code: " << res << std::endl; + } + out << "Write to registry key completed" << std::endl; } else { @@ -137,6 +212,80 @@ void WriteToUninstallRegistry( RegCloseKey(hkey); } +void WriteToFile(const path& filePath, const std::wstringstream& content) +{ + std::wofstream file(filePath, std::ofstream::out); + file << content.str(); + file.close(); +} + +void HandleRepairOperation(const std::wstring& productID, const std::wstringstream& outContent, bool useHKLM) +{ + path installDirectory; + + // Open the registry key + HKEY hKey; + std::wstring registryPath = std::wstring(RegistrySubkey); + + if (!productID.empty()) + { + registryPath += productID; + } + else + { + registryPath += DefaultProductID; + } + + LONG lReg = RegOpenKeyEx(useHKLM ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, registryPath.c_str(), 0, KEY_READ, &hKey); + + if (lReg == ERROR_SUCCESS) + { + // Query the value of the InstallLocation + wchar_t regInstallLocation[MAX_PATH]; + DWORD bufferSize = sizeof(regInstallLocation); + lReg = RegQueryValueEx(hKey, L"InstallLocation", NULL, NULL, (LPBYTE)regInstallLocation, &bufferSize); + + if (lReg == ERROR_SUCCESS) + { + // Convert the InstallLocation to a path + installDirectory = std::wstring(regInstallLocation); + } + + // Close the registry key + RegCloseKey(hKey); + + if(installDirectory.empty()) + { + // We could not find the install location, so we cannot repair + return; + } + } + else + { + // We could not find the uninstall APR registry key, so we cannot repair + return; + } + + path outFilePath = installDirectory; + outFilePath /= "TestExeRepairCompleted.txt"; + WriteToFile(outFilePath, outContent); +} + +void HandleInstallationOperation(std::wostream& out, const path& installDirectory, const std::wstringstream& outContent, const std::wstring& productCode, bool useHKLM, const std::wstring& displayName, const std::wstring& displayVersion) +{ + path outFilePath = installDirectory; + outFilePath /= "TestExeInstalled.txt"; + + std::wofstream file(outFilePath, std::ofstream::out); + file << outContent.str(); + file.close(); + + path uninstallerPath = GenerateUninstaller(out, installDirectory, productCode, useHKLM); + path modifyPath = GenerateModifyPath(installDirectory); + + WriteToUninstallRegistry(out, productCode, uninstallerPath, modifyPath, displayName, displayVersion, installDirectory.wstring(), useHKLM); +} + // The installer prints all args to an output file and writes to the Uninstall registry key int wmain(int argc, const wchar_t** argv) { @@ -150,6 +299,7 @@ int wmain(int argc, const wchar_t** argv) bool useHKLM = false; bool noOperation = false; int exitCode = 0; + bool isRepair = false; // Output to cout by default, but swap to a file if requested std::wostream* out = &std::wcout; @@ -246,6 +396,13 @@ int wmain(int argc, const wchar_t** argv) } } + // Supports /repair and /r to emulate repair operation using installer. + else if (_wcsicmp(argv[i], L"/repair") == 0 + || _wcsicmp(argv[i], L"/r") == 0) + { + isRepair = true; + } + // Returns the success exit code to emulate being invoked by another caller. else if (_wcsicmp(argv[i], L"/NoOperation") == 0) { @@ -264,7 +421,7 @@ int wmain(int argc, const wchar_t** argv) execInfo.cbSize = sizeof(execInfo); execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; execInfo.lpFile = aliasToExecute.c_str(); - + if (!aliasArguments.empty()) { execInfo.lpParameters = aliasArguments.c_str(); @@ -288,16 +445,16 @@ int wmain(int argc, const wchar_t** argv) } path outFilePath = installDirectory; - outFilePath /= "TestExeInstalled.txt"; - std::wofstream file(outFilePath, std::ofstream::out); - - file << outContent.str(); - - file.close(); - - path uninstallerPath = GenerateUninstaller(*out, installDirectory, productCode, useHKLM); - WriteToUninstallRegistry(*out, productCode, uninstallerPath, displayName, displayVersion, installDirectory.wstring(), useHKLM); + if (isRepair) + { + outContent << L"\nInstaller Repair operation for AppInstallerTestExeInstaller.exe completed successfully."; + HandleRepairOperation(productCode, outContent, useHKLM); + } + else + { + HandleInstallationOperation(*out, installDirectory, outContent, productCode, useHKLM, displayName, displayVersion); + } return exitCode; } From c928c65b465e13ee181f3301a8422e4311ca8807 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Mon, 3 Jun 2024 18:14:52 -0700 Subject: [PATCH 03/17] Winget Repair E2E Test cases - Added E2E tests for winget, targeting installer types such as - MSI, - NonStore MSIX, - Burn, Nullsoft, Inno, and Exe. - These tests aim to ensure reliability across Modify Repair, Uninstaller Repair, and Installer Repair scenarios. --- .../AppInstallerCLIE2ETests.csproj | 8 + src/AppInstallerCLIE2ETests/Constants.cs | 3 + .../Helpers/TestCommon.cs | 120 ++++++++++++- .../Helpers/TestIndex.cs | 25 ++- .../Helpers/TestSetup.cs | 6 + src/AppInstallerCLIE2ETests/RepairCommand.cs | 167 ++++++++++++++++++ .../AppInstallerTestMsiInstallerV2.msi | Bin 0 -> 219136 bytes .../TestBurnInstaller.ModifyRepair.yaml | 19 ++ .../TestExeInstaller.UninstallerRepair.yaml | 19 ++ .../TestInnoInstaller.InstallerRepair.yaml | 19 ++ .../Manifests/TestMsiInstaller.Repair.yaml | 14 ++ ...stNullsoftInstaller.UninstallerRepair.yaml | 19 ++ .../TestData/localsource.json | 6 + 13 files changed, 419 insertions(+), 6 deletions(-) create mode 100644 src/AppInstallerCLIE2ETests/RepairCommand.cs create mode 100644 src/AppInstallerCLIE2ETests/TestData/AppInstallerTestMsiInstallerV2.msi create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepair.yaml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepair.yaml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestInnoInstaller.InstallerRepair.yaml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.Repair.yaml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestNullsoftInstaller.UninstallerRepair.yaml diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index 7a724c4a23..a87e043719 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -50,8 +50,13 @@ + + + + + PreserveNewest @@ -77,6 +82,9 @@ PreserveNewest + + PreserveNewest + diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 61cf60eb95..52e8ec1029 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -25,6 +25,7 @@ public class Constants public const string LocalServerCertPathParameter = "LocalServerCertPath"; public const string ExeInstallerPathParameter = "ExeTestInstallerPath"; public const string MsiInstallerPathParameter = "MsiTestInstallerPath"; + public const string MsiInstallerV2PathParameter = "MsiTestInstallerV2Path"; public const string MsixInstallerPathParameter = "MsixTestInstallerPath"; public const string PackageCertificatePathParameter = "PackageCertificatePath"; public const string PowerShellModulePathParameter = "PowerShellModulePath"; @@ -58,6 +59,7 @@ public class Constants public const string ZipInstaller = "AppInstallerTestZipInstaller"; public const string ExeInstallerFileName = "AppInstallerTestExeInstaller.exe"; public const string MsiInstallerFileName = "AppInstallerTestMsiInstaller.msi"; + public const string MsiInstallerV2FileName = "AppInstallerTestMsiInstallerV2.msi"; public const string MsixInstallerFileName = "AppInstallerTestMsixInstaller.msix"; public const string ZipInstallerFileName = "AppInstallerTestZipInstaller.zip"; public const string IndexPackage = "source.msix"; @@ -91,6 +93,7 @@ public class Constants public const string TestExeInstalledFileName = "TestExeInstalled.txt"; public const string TestExeUninstallerFileName = "UninstallTestExe.bat"; public const string TestExeUninstalledFileName = "TestExeUninstalled.txt"; + public const string TestExeRepairCompletedFileName = "TestExeRepairCompleted.txt"; // PowerShell Cmdlets public const string FindCmdlet = "Find-WinGetPackage"; diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index ddad1a1d2a..96df68e808 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -16,6 +16,7 @@ namespace AppInstallerCLIE2ETests.Helpers using System.Security.Principal; using System.Text; using System.Threading; + using System.Web; using AppInstallerCLIE2ETests; using AppInstallerCLIE2ETests.PowerShell; using Microsoft.Management.Deployment; @@ -470,6 +471,32 @@ public static bool VerifyTestExeInstalled(string installDir, string expectedCont return verifyInstallSuccess; } + /// + /// Verifies if the repair of the test executable was successful. + /// + /// The directory where the test executable is installed. + /// The expected content in the test executable file. This is optional. + /// Returns true if the repair was successful, false otherwise. + public static bool VerifyTestExeRepairSuccessful(string installDir, string expectedContent = null) + { + bool verifyRepairSuccess = true; + + if (!File.Exists(Path.Combine(installDir, Constants.TestExeRepairCompletedFileName))) + { + TestContext.Out.WriteLine($"{Constants.TestExeRepairCompletedFileName} not found at {installDir}"); + verifyRepairSuccess = false; + } + + if (verifyRepairSuccess && !string.IsNullOrEmpty(expectedContent)) + { + string content = File.ReadAllText(Path.Combine(installDir, Constants.TestExeRepairCompletedFileName)); + TestContext.Out.WriteLine($"TestExeRepairCompleted.txt content: {content}"); + verifyRepairSuccess = content.Contains(expectedContent); + } + + return verifyRepairSuccess; + } + /// /// Verify installer and manifest downloaded correctly and cleanup. /// @@ -543,6 +570,19 @@ public static bool VerifyTestExeInstalled(string installDir, string expectedCont return downloadResult; } + /// + /// Best effort test exe cleanup. + /// + /// Install directory. + public static void BestEffortTestExeCleanup(string installDir) + { + var uninstallerPath = Path.Combine(installDir, Constants.TestExeUninstallerFileName); + if (File.Exists(uninstallerPath)) + { + RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); + } + } + /// /// Verify exe installer correctly and then uninstall it. /// @@ -554,15 +594,30 @@ public static bool VerifyTestExeInstalledAndCleanup(string installDir, string ex bool verifyInstallSuccess = VerifyTestExeInstalled(installDir, expectedContent); // Always try clean up and ignore clean up failure - var uninstallerPath = Path.Combine(installDir, Constants.TestExeUninstallerFileName); - if (File.Exists(uninstallerPath)) - { - RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); - } + BestEffortTestExeCleanup(installDir); return verifyInstallSuccess; } + /// + /// Verify exe repair completed and cleanup. + /// + /// Install directory. + /// Optional expected context. + /// True if success. + public static bool VerifyTestExeRepairCompletedAndCleanup(string installDir, string expectedContent = null) + { + bool verifyRepairSuccess = VerifyTestExeRepairSuccessful(installDir, expectedContent); + + // Always try clean up and ignore clean up failure + BestEffortTestExeCleanup(installDir); + + // Delete the install directory to reclaim disk space + Directory.Delete(installDir, true); + + return verifyRepairSuccess; + } + /// /// Verify msi installed correctly. /// @@ -912,6 +967,61 @@ public static string GetExpectedModulePath(TestModuleLocation location) } } + /// + /// Copy the installer file to the ARP InstallSource directory. + /// + /// Test installer to be copied. + /// Installer Product. + /// is WoW6432Node to use. + /// Returns the installer source directory if the file operation is successful, otherwise returns an empty string. + public static string CopyInstallerFileToARPInstallSourceDirectory(string installerFilePath, string productCode, bool useWoW6432Node = false) + { + if (string.IsNullOrEmpty(installerFilePath)) + { + new ArgumentNullException(nameof(installerFilePath)); + } + + if (!File.Exists(installerFilePath)) + { + new FileNotFoundException(installerFilePath); + } + + string outputDirectory = string.Empty; + + // Define the registry paths for both x64 and x86 + string registryPath = useWoW6432Node + ? $@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{productCode}" + : $@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{productCode}"; + + // Open the registry key where the uninstall information is stored + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath)) + { + if (key != null) + { + // Read the InstallSource value + string arpInstallSourceDirectory = key.GetValue("InstallSource") as string; + + if (!string.IsNullOrEmpty(arpInstallSourceDirectory)) + { + // Copy the MSI installer to the InstallSource directory + string installerFileName = Path.GetFileName(installerFilePath); + string installerDestinationPath = Path.Combine(arpInstallSourceDirectory, installerFileName); + + if (!Directory.Exists(arpInstallSourceDirectory)) + { + Directory.CreateDirectory(arpInstallSourceDirectory); + } + + File.Copy(installerFilePath, installerDestinationPath, true); + + outputDirectory = arpInstallSourceDirectory; + } + } + } + + return outputDirectory; + } + /// /// Run winget command via direct process. /// diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs b/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs index 91931403f8..2728661e86 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -22,6 +22,7 @@ static TestIndex() // Expected path for the installers. TestIndex.ExeInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.ExeInstaller, Constants.ExeInstallerFileName); TestIndex.MsiInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsiInstaller, Constants.MsiInstallerFileName); + TestIndex.MsiInstallerV2 = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsiInstaller, Constants.MsiInstallerV2FileName); TestIndex.MsixInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsixInstaller, Constants.MsixInstallerFileName); TestIndex.ZipInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.ZipInstaller, Constants.ZipInstallerFileName); } @@ -36,6 +37,11 @@ static TestIndex() /// public static string MsiInstaller { get; private set; } + /// + /// Gets the signed msi installerV2 path used by the manifests in the E2E test. + /// + public static string MsiInstallerV2 { get; private set; } + /// /// Gets the signed msix installer path used by the manifests in the E2E test. /// @@ -73,6 +79,16 @@ public static void GenerateE2ESource() throw new FileNotFoundException(testParams.MsiInstallerPath); } + if (string.IsNullOrEmpty(testParams.MsiInstallerV2Path)) + { + throw new ArgumentNullException($"{Constants.MsiInstallerV2PathParameter} is required"); + } + + if (!File.Exists(testParams.MsiInstallerV2Path)) + { + throw new FileNotFoundException(testParams.MsiInstallerV2Path); + } + if (string.IsNullOrEmpty(testParams.MsixInstallerPath)) { throw new ArgumentNullException($"{Constants.MsixInstallerPathParameter} is required"); @@ -118,6 +134,13 @@ public static void GenerateE2ESource() HashToken = "", }, new LocalInstaller + { + Type = InstallerType.Msi, + Name = Path.Combine(Constants.MsiInstaller, Constants.MsiInstallerV2FileName), + Input = testParams.MsiInstallerPath, + HashToken = "", + }, + new LocalInstaller { Type = InstallerType.Msix, Name = Path.Combine(Constants.MsixInstaller, Constants.MsixInstallerFileName), diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs index 2e81098f8e..b681fc710f 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs @@ -43,6 +43,7 @@ private TestSetup() this.ExeInstallerPath = this.InitializeFileParam(Constants.ExeInstallerPathParameter); this.MsiInstallerPath = this.InitializeFileParam(Constants.MsiInstallerPathParameter); this.MsixInstallerPath = this.InitializeFileParam(Constants.MsixInstallerPathParameter); + this.MsiInstallerV2Path = this.InitializeFileParam(Constants.MsiInstallerV2PathParameter); this.ForcedExperimentalFeatures = this.InitializeStringArrayParam(Constants.ForcedExperimentalFeaturesParameter); } @@ -103,6 +104,11 @@ public static TestSetup Parameters /// public string MsiInstallerPath { get; } + /// + /// Gets the msi installer V2 path. + /// + public string MsiInstallerV2Path { get; } + /// /// Gets the msix installer path. /// diff --git a/src/AppInstallerCLIE2ETests/RepairCommand.cs b/src/AppInstallerCLIE2ETests/RepairCommand.cs new file mode 100644 index 0000000000..b365cb58c2 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/RepairCommand.cs @@ -0,0 +1,167 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System; + using System.IO; + using AppInstallerCLIE2ETests.Helpers; + using Markdig.Extensions.Figures; + using NUnit.Framework; + using WinGetSourceCreator.Model; + + /// + /// Test Repair command. + /// + public class RepairCommand : BaseCommand + { + /// + /// One time setup. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + } + + /// + /// Set up. + /// + [SetUp] + public void Setup() + { + } + + /// + /// Test MSI installer repair. + /// + [Test] + public void RepairMSIInstaller() + { + if (string.IsNullOrEmpty(TestIndex.MsiInstallerV2)) + { + Assert.Ignore("MSI installer not available"); + } + + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestMsiRepair --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + // 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); + + result = TestCommon.RunAICLICommand("repair", $"AppInstallerTest.TestMsiRepair"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Repair operation completed successfully")); + Assert.True(TestCommon.VerifyTestMsiInstalledAndCleanup(installDir)); + + if (installerSourceDir != null && Directory.Exists(installerSourceDir)) + { + Directory.Delete(installerSourceDir, true); + } + } + + /// + /// Test MSIX non-store package repair. + /// + [Test] + public void RepairNonStoreMSIXPackage() + { + // install a test msix package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestMsixInstaller --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestMsixInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Repair operation completed successfully")); + Assert.True(TestCommon.VerifyTestMsixInstalledAndCleanup()); + } + + /// + /// Test repair of a Burn installer that has a "modify" repair behavior specified in the manifest. + /// + [Test] + public void RepairBurnInstallerWithModifyBehavior() + { + // install a test burn package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestModifyRepair -v 2.0.0.0 --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestModifyRepair"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Repair operation completed successfully")); + Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(installDir, "Modify Repair operation")); + } + + /// + /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest. + /// + [Test] + public void RepairExeInstallerWithUninstallerBehavior() + { + // install a test Exe package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.UninstallerRepair -v 2.0.0.0 --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.UninstallerRepair"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Repair operation completed successfully")); + Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(installDir, "Uninstaller Repair operation")); + } + + /// + /// Test repair of a Nullsoft installer that has a "uninstaller" repair behavior specified in the manifest. + /// + [Test] + public void RepairNullsoftInstallerWithUninstallerBehavior() + { + // install a test Nullsoft package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.UninstallerRepair -v 2.0.0.0 --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.UninstallerRepair"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Repair operation completed successfully")); + Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(installDir, "Uninstaller Repair operation")); + } + + /// + /// Test repair of a Inno installer that has a "installer" repair behavior specified in the manifest. + /// + [Test] + public void RepairInnoInstallerWithInstallerBehavior() + { + // install a test Inno package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestInstallerRepair -v 2.0.0.0 --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestInstallerRepair"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Repair operation completed successfully")); + Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(installDir, "Installer Repair operation")); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/TestData/AppInstallerTestMsiInstallerV2.msi b/src/AppInstallerCLIE2ETests/TestData/AppInstallerTestMsiInstallerV2.msi new file mode 100644 index 0000000000000000000000000000000000000000..0024a35649300226448b8aab3117e4b831d649ba GIT binary patch literal 219136 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 N?cZOg|NFm2;BSg&8CU=S literal 0 HcmV?d00001 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepair.yaml new file mode 100644 index 0000000000..70b44ec797 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepair.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerTest.TestModifyRepair +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: TestModifyRepair +Publisher: AppInstallerTest +RepairBehavior: modify +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: burn + InstallerSha256: + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ElevationRequirement: elevationRequired + InstallerSwitches: + InstallLocation: /InstallDir + Custom: /Version 2.0.0.0 /DisplayName TestModifyRepair /UseHKLM + Repair: /repair +ManifestType: singleton +ManifestVersion: 1.7.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepair.yaml new file mode 100644 index 0000000000..6bd501d5c1 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepair.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerTest.UninstallerRepair +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: UninstallerRepair +Publisher: AppInstallerTest +RepairBehavior: uninstaller +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ElevationRequirement: elevationRequired + InstallerSwitches: + InstallLocation: /InstallDir + Custom: /Version 2.0.0.0 /DisplayName UninstallerRepair /UseHKLM + Repair: /repair +ManifestType: singleton +ManifestVersion: 1.7.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestInnoInstaller.InstallerRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestInnoInstaller.InstallerRepair.yaml new file mode 100644 index 0000000000..a43a34722b --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestInnoInstaller.InstallerRepair.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerTest.TestInstallerRepair +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: TestInstallerRepair +Publisher: AppInstallerTest +RepairBehavior: installer +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: inno + InstallerSha256: + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ElevationRequirement: elevationRequired + InstallerSwitches: + InstallLocation: /InstallDir + Custom: /Version 2.0.0.0 /DisplayName TestInstallerRepair /UseHKLM + Repair: /repair /UseHKLM +ManifestType: singleton +ManifestVersion: 1.7.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.Repair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.Repair.yaml new file mode 100644 index 0000000000..1ae08edcea --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.Repair.yaml @@ -0,0 +1,14 @@ +# Uses the MSI installer; +PackageIdentifier: AppInstallerTest.TestMsiRepair +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: TestMsiInstallerV2 +Publisher: AppInstallerTest +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestMsiInstaller/AppInstallerTestMsiInstallerV2.msi + InstallerType: msi + InstallerSha256: + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' +ManifestType: singleton +ManifestVersion: 1.7.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestNullsoftInstaller.UninstallerRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestNullsoftInstaller.UninstallerRepair.yaml new file mode 100644 index 0000000000..c1444e6626 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestNullsoftInstaller.UninstallerRepair.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerTest.NullsoftUninstallerRepair +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: NullsoftUninstallerRepair +Publisher: AppInstallerTest +RepairBehavior: uninstaller +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ElevationRequirement: elevationRequired + InstallerSwitches: + InstallLocation: /InstallDir + Custom: /Version 2.0.0.0 /DisplayName NullsoftUninstallerRepair /UseHKLM + Repair: /repair +ManifestType: singleton +ManifestVersion: 1.7.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/localsource.json b/src/AppInstallerCLIE2ETests/TestData/localsource.json index adb672f0fc..45bae359be 100644 --- a/src/AppInstallerCLIE2ETests/TestData/localsource.json +++ b/src/AppInstallerCLIE2ETests/TestData/localsource.json @@ -17,6 +17,12 @@ "Input": "%BUILD_SOURCESDIRECTORY%/src/AppInstallerCLIE2ETests/TestData/AppInstallerTestMsiInstaller.msi", "HashToken": "" }, + { + "Type": "msi", + "Name": "AppInstallerTestMsiInstaller/AppInstallerTestMsiInstallerV2.msi", + "Input": "%BUILD_SOURCESDIRECTORY%/src/AppInstallerCLIE2ETests/TestData/AppInstallerTestMsiInstallerV2.msi", + "HashToken": "" + }, { "Type": "msix", "Name": "AppInstallerTestMsixInstaller/AppInstallerTestMsixInstaller.msix", From 470856c0bd878328ac9904c17b52836982347631 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Mon, 3 Jun 2024 21:03:15 -0700 Subject: [PATCH 04/17] Fix: Remove unused namespaces causing spell check errors --- src/AppInstallerCLIE2ETests/RepairCommand.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/RepairCommand.cs b/src/AppInstallerCLIE2ETests/RepairCommand.cs index b365cb58c2..354737f093 100644 --- a/src/AppInstallerCLIE2ETests/RepairCommand.cs +++ b/src/AppInstallerCLIE2ETests/RepairCommand.cs @@ -6,12 +6,9 @@ namespace AppInstallerCLIE2ETests { - using System; using System.IO; using AppInstallerCLIE2ETests.Helpers; - using Markdig.Extensions.Figures; using NUnit.Framework; - using WinGetSourceCreator.Model; /// /// Test Repair command. From 78e76136df02c9a9d774811e7d645be8b4b354c8 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Mon, 3 Jun 2024 23:02:44 -0700 Subject: [PATCH 05/17] Fix: To address MSI repair test failures, AppInstallerTest.TestMsiInstaller will be uninstalled as part of test setup to avoid interference. --- src/AppInstallerCLIE2ETests/RepairCommand.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AppInstallerCLIE2ETests/RepairCommand.cs b/src/AppInstallerCLIE2ETests/RepairCommand.cs index 354737f093..a86c7af821 100644 --- a/src/AppInstallerCLIE2ETests/RepairCommand.cs +++ b/src/AppInstallerCLIE2ETests/RepairCommand.cs @@ -21,6 +21,8 @@ public class RepairCommand : BaseCommand [OneTimeSetUp] public void OneTimeSetup() { + // Try clean up AppInstallerTest.TestMsiInstaller for failure cases where cleanup is not successful + TestCommon.RunAICLICommand("uninstall", "AppInstallerTest.TestMsiInstaller"); } /// From fec6f13326422554d0a3099c7e6bed1c2c357126 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Wed, 5 Jun 2024 15:00:16 -0700 Subject: [PATCH 06/17] Extend NoRepair and NoModify ARP support for AppInstallerTestExeInstaller --- src/AppInstallerTestExeInstaller/main.cpp | 42 ++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp index 8df9447472..7ead74755d 100644 --- a/src/AppInstallerTestExeInstaller/main.cpp +++ b/src/AppInstallerTestExeInstaller/main.cpp @@ -121,7 +121,9 @@ void WriteToUninstallRegistry( const std::wstring& displayName, const std::wstring& displayVersion, const std::wstring& installLocation, - bool useHKLM) + bool useHKLM, + bool noRepair, + bool noModify) { HKEY hkey; LONG lReg; @@ -203,6 +205,26 @@ void WriteToUninstallRegistry( out << "Failed to write ModifyPath value. Error Code: " << res << std::endl; } + if(noRepair) + { + // Set NoRepair Property Value + DWORD noRepairValue = 1; + if (LONG res = RegSetValueEx(hkey, L"NoRepair", NULL, REG_DWORD, (LPBYTE)&noRepairValue, sizeof(noRepairValue)) != ERROR_SUCCESS) + { + out << "Failed to write NoRepair value. Error Code: " << res << std::endl; + } + } + + if(noModify) + { + // Set NoModify Property Value + DWORD noModifyValue = 1; + if (LONG res = RegSetValueEx(hkey, L"NoModify", NULL, REG_DWORD, (LPBYTE)&noModifyValue, sizeof(noModifyValue)) != ERROR_SUCCESS) + { + out << "Failed to write NoModify value. Error Code: " << res << std::endl; + } + } + out << "Write to registry key completed" << std::endl; } else { @@ -271,7 +293,7 @@ void HandleRepairOperation(const std::wstring& productID, const std::wstringstre WriteToFile(outFilePath, outContent); } -void HandleInstallationOperation(std::wostream& out, const path& installDirectory, const std::wstringstream& outContent, const std::wstring& productCode, bool useHKLM, const std::wstring& displayName, const std::wstring& displayVersion) +void HandleInstallationOperation(std::wostream& out, const path& installDirectory, const std::wstringstream& outContent, const std::wstring& productCode, bool useHKLM, const std::wstring& displayName, const std::wstring& displayVersion, bool noRepair, bool noModify) { path outFilePath = installDirectory; outFilePath /= "TestExeInstalled.txt"; @@ -283,7 +305,7 @@ void HandleInstallationOperation(std::wostream& out, const path& installDirector path uninstallerPath = GenerateUninstaller(out, installDirectory, productCode, useHKLM); path modifyPath = GenerateModifyPath(installDirectory); - WriteToUninstallRegistry(out, productCode, uninstallerPath, modifyPath, displayName, displayVersion, installDirectory.wstring(), useHKLM); + WriteToUninstallRegistry(out, productCode, uninstallerPath, modifyPath, displayName, displayVersion, installDirectory.wstring(), useHKLM, noRepair, noModify); } // The installer prints all args to an output file and writes to the Uninstall registry key @@ -300,6 +322,8 @@ int wmain(int argc, const wchar_t** argv) bool noOperation = false; int exitCode = 0; bool isRepair = false; + bool noRepair = false; + bool noModify = false; // Output to cout by default, but swap to a file if requested std::wostream* out = &std::wcout; @@ -403,6 +427,16 @@ int wmain(int argc, const wchar_t** argv) isRepair = true; } + else if (_wcsicmp(argv[i], L"/NoRepair") == 0) + { + noRepair = true; + } + + else if (_wcsicmp(argv[i], L"/NoModify") == 0) + { + noModify = true; + } + // Returns the success exit code to emulate being invoked by another caller. else if (_wcsicmp(argv[i], L"/NoOperation") == 0) { @@ -453,7 +487,7 @@ int wmain(int argc, const wchar_t** argv) } else { - HandleInstallationOperation(*out, installDirectory, outContent, productCode, useHKLM, displayName, displayVersion); + HandleInstallationOperation(*out, installDirectory, outContent, productCode, useHKLM, displayName, displayVersion, noRepair, noModify); } return exitCode; From cb35e5a4e807ba7edab9957b245ad73b408286a1 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Wed, 5 Jun 2024 15:04:29 -0700 Subject: [PATCH 07/17] Extend winget repair tests to include negative test scenarios --- .../AppInstallerCLIE2ETests.csproj | 4 + src/AppInstallerCLIE2ETests/Constants.cs | 4 + .../Helpers/TestCommon.cs | 23 +++- src/AppInstallerCLIE2ETests/RepairCommand.cs | 122 ++++++++++++++++++ ...stBurnInstaller.MisisngRepairBehavior.yaml | 18 +++ ...urnInstaller.ModifyRepairWithNoModify.yaml | 19 +++ ....UserScopeInstallRepairInAdminContext.yaml | 19 +++ ...staller.UninstallerRepairWithNoRepair.yaml | 19 +++ 8 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MisisngRepairBehavior.yaml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepairWithNoModify.yaml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.UserScopeInstallRepairInAdminContext.yaml create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepairWithNoRepair.yaml diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index a87e043719..1cca606ebc 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -53,8 +53,12 @@ + + + + diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 52e8ec1029..fdc0a3e111 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -266,6 +266,10 @@ public class ErrorCode public const int ERROR_INVALID_RESUME_STATE = unchecked((int)0x8A150070); public const int ERROR_CANNOT_OPEN_CHECKPOINT_INDEX = unchecked((int)0x8A150071); + public const int ERROR_NO_REPAIR_INFO_FOUND = unchecked((int)0x8A150079); + public const int ERROR_REPAIR_NOT_SUPPORTED = unchecked((int)0x8A15007C); + public const int ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED = unchecked((int)0x8A15007D); + public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); public const int ERROR_INSTALL_FILE_IN_USE = unchecked((int)0x8A150103); diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 96df68e808..9d60109edc 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -583,6 +583,22 @@ public static void BestEffortTestExeCleanup(string installDir) } } + /// + /// Best effort test exe cleanup and install directory cleanup. + /// + /// Install directory. + public static void CleanupTestExeAndDirectory(string installDir) + { + // Always try clean up and ignore clean up failure + BestEffortTestExeCleanup(installDir); + + // Delete the install directory to reclaim disk space + if (Directory.Exists(installDir)) + { + Directory.Delete(installDir, true); + } + } + /// /// Verify exe installer correctly and then uninstall it. /// @@ -608,12 +624,7 @@ public static bool VerifyTestExeInstalledAndCleanup(string installDir, string ex public static bool VerifyTestExeRepairCompletedAndCleanup(string installDir, string expectedContent = null) { bool verifyRepairSuccess = VerifyTestExeRepairSuccessful(installDir, expectedContent); - - // Always try clean up and ignore clean up failure - BestEffortTestExeCleanup(installDir); - - // Delete the install directory to reclaim disk space - Directory.Delete(installDir, true); + CleanupTestExeAndDirectory(installDir); return verifyRepairSuccess; } diff --git a/src/AppInstallerCLIE2ETests/RepairCommand.cs b/src/AppInstallerCLIE2ETests/RepairCommand.cs index a86c7af821..24fc52205c 100644 --- a/src/AppInstallerCLIE2ETests/RepairCommand.cs +++ b/src/AppInstallerCLIE2ETests/RepairCommand.cs @@ -87,6 +87,28 @@ public void RepairNonStoreMSIXPackage() Assert.True(TestCommon.VerifyTestMsixInstalledAndCleanup()); } + /// + /// Test MSIX non-store package repair with machine scope. + /// + [Test] + public void RepairNonStoreMsixPackageWithMachineScope() + { + // Selecting Microsoft.Paint_8wekyb3d8bbwe because it's a system package suitable for this scenario. + // First, we need to ensure this package is installed, otherwise, we skip the test. + var result = TestCommon.RunAICLICommand("list", $"Microsoft.Paint_8wekyb3d8bbwe"); + + if (result.ExitCode != Constants.ErrorCode.S_OK) + { + Assert.Ignore("Test skipped as Microsoft.Paint_8wekyb3d8bbwe is not installed."); + } + + Assert.True(result.StdOut.Contains("Microsoft.Paint_8wekyb3d8bbwe")); + + result = TestCommon.RunAICLICommand("repair", $"Microsoft.Paint_8wekyb3d8bbwe --scope machine"); + Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALL_SYSTEM_NOT_SUPPORTED, result.ExitCode); + Assert.True(result.StdOut.Contains("The current system configuration does not support the repair of this package.")); + } + /// /// Test repair of a Burn installer that has a "modify" repair behavior specified in the manifest. /// @@ -106,6 +128,87 @@ public void RepairBurnInstallerWithModifyBehavior() Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(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. + /// + [Test] + public void RepairBurnInstallerInAdminContextWithUserScopeInstall() + { + // install a test burn package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestUserScopeInstallRepairInAdminContext -v 2.0.0.0 --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestUserScopeInstallRepairInAdminContext"); + Assert.AreEqual(Constants.ErrorCode.ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED, result.ExitCode); + Assert.True(result.StdOut.Contains("The package installed for user scope cannot be repaired when running with administrator privileges.")); + TestCommon.CleanupTestExeAndDirectory(installDir); + } + + /// + /// Tests the repair operation of a Burn installer that lacks a repair behavior. + /// + [Test] + public void RepairBurnInstallerMissingRepairBehavior() + { + // install a test burn package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestMissingRepairBehavior -v 2.0.0.0 --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestMissingRepairBehavior"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_REPAIR_INFO_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("The repair command for this package cannot be found. Please reach out to the package publisher for support.")); + TestCommon.CleanupTestExeAndDirectory(installDir); + } + + /// + /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest and NoModify ARP flag set. + /// + [Test] + public void RepairBurnInstallerWithWithModifyBehaviorAndNoModifyFlag() + { + // install a test Exe package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestModifyRepairWithNoModify -v 2.0.0.0 --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestModifyRepairWithNoModify"); + Assert.AreEqual(Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED, result.ExitCode); + Assert.True(result.StdOut.Contains("The installer technology in use does not support repair.")); + TestCommon.CleanupTestExeAndDirectory(installDir); + } + + /// + /// Tests the scenario where the repair operation is not supported for Portable Installer type. + /// + [Test] + public void RepairOperationNotSupportedForPortableInstaller() + { + string installDir = TestCommon.GetPortablePackagesDirectory(); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestPortableExe"); + Assert.AreEqual(Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED, result.ExitCode); + Assert.True(result.StdOut.Contains("The installer technology in use does not support repair.")); + + // 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. /// @@ -125,6 +228,25 @@ public void RepairExeInstallerWithUninstallerBehavior() Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(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. + /// + [Test] + public void RepairExeInstallerWithUninstallerBehaviorAndNoRepairFlag() + { + // install a test Exe package from TestSource and then repair it. + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.UninstallerRepairWithNoRepair -v 2.0.0.0 --silent -l {installDir}"); + + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.UninstallerRepairWithNoRepair"); + Assert.AreEqual(Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED, result.ExitCode); + Assert.True(result.StdOut.Contains("The installer technology in use does not support repair.")); + TestCommon.CleanupTestExeAndDirectory(installDir); + } + /// /// Test repair of a Nullsoft installer that has a "uninstaller" repair behavior specified in the manifest. /// diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MisisngRepairBehavior.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MisisngRepairBehavior.yaml new file mode 100644 index 0000000000..d490a7b4d1 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MisisngRepairBehavior.yaml @@ -0,0 +1,18 @@ +PackageIdentifier: AppInstallerTest.TestMissingRepairBehavior +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: TestMissingRepairBehavior +Publisher: AppInstallerTest +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: burn + InstallerSha256: + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ElevationRequirement: elevationRequired + InstallerSwitches: + InstallLocation: /InstallDir + Custom: /Version 2.0.0.0 /DisplayName TestMissingRepairBehavior /UseHKLM + Repair: /repair +ManifestType: singleton +ManifestVersion: 1.7.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepairWithNoModify.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepairWithNoModify.yaml new file mode 100644 index 0000000000..65a93af82f --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.ModifyRepairWithNoModify.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerTest.TestModifyRepairWithNoModify +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: TestModifyRepairWithNoModify +Publisher: AppInstallerTest +RepairBehavior: modify +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: burn + InstallerSha256: + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ElevationRequirement: elevationRequired + InstallerSwitches: + InstallLocation: /InstallDir + Custom: /Version 2.0.0.0 /DisplayName TestModifyRepairWithNoModify /UseHKLM /NoModify + Repair: /repair +ManifestType: singleton +ManifestVersion: 1.7.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.UserScopeInstallRepairInAdminContext.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.UserScopeInstallRepairInAdminContext.yaml new file mode 100644 index 0000000000..9255693e78 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.UserScopeInstallRepairInAdminContext.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerTest.TestUserScopeInstallRepairInAdminContext +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: TestUserScopeInstallRepairInAdminContext +Publisher: AppInstallerTest +RepairBehavior: modify +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: burn + InstallerSha256: + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ElevationRequirement: elevationRequired + InstallerSwitches: + InstallLocation: /InstallDir + Custom: /Version 2.0.0.0 /DisplayName TestUserScopeInstallRepairInAdminContext + Repair: /repair +ManifestType: singleton +ManifestVersion: 1.7.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepairWithNoRepair.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepairWithNoRepair.yaml new file mode 100644 index 0000000000..e816c82e04 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.UninstallerRepairWithNoRepair.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerTest.UninstallerRepairWithNoRepair +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: UninstallerRepairWithNoRepair +Publisher: AppInstallerTest +RepairBehavior: uninstaller +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' + ElevationRequirement: elevationRequired + InstallerSwitches: + InstallLocation: /InstallDir + Custom: /Version 2.0.0.0 /DisplayName UninstallerRepairWithNoRepair /UseHKLM /NoRepair + Repair: /repair +ManifestType: singleton +ManifestVersion: 1.7.0 From 1fa5ac0d251676df6697f5d82be21b6d4fa04693 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Wed, 5 Jun 2024 15:12:28 -0700 Subject: [PATCH 08/17] Resolving Spellcheck error by changing the test manifest name to 'TestBurnInstaller.MissingRepairBehavior.yaml' --- src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj | 2 +- ...havior.yaml => TestBurnInstaller.MissingRepairBehavior.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/AppInstallerCLIE2ETests/TestData/Manifests/{TestBurnInstaller.MisisngRepairBehavior.yaml => TestBurnInstaller.MissingRepairBehavior.yaml} (100%) diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index 1cca606ebc..9cabdf036c 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -53,7 +53,7 @@ - + diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MisisngRepairBehavior.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MissingRepairBehavior.yaml similarity index 100% rename from src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MisisngRepairBehavior.yaml rename to src/AppInstallerCLIE2ETests/TestData/Manifests/TestBurnInstaller.MissingRepairBehavior.yaml From bb5acc4062aceb907a5fdcab28665a68f4f9a0d8 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Thu, 6 Jun 2024 13:22:21 -0700 Subject: [PATCH 09/17] Clean up unused test Setup method --- src/AppInstallerCLIE2ETests/RepairCommand.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/RepairCommand.cs b/src/AppInstallerCLIE2ETests/RepairCommand.cs index 24fc52205c..289a0e0ee4 100644 --- a/src/AppInstallerCLIE2ETests/RepairCommand.cs +++ b/src/AppInstallerCLIE2ETests/RepairCommand.cs @@ -25,14 +25,6 @@ public void OneTimeSetup() TestCommon.RunAICLICommand("uninstall", "AppInstallerTest.TestMsiInstaller"); } - /// - /// Set up. - /// - [SetUp] - public void Setup() - { - } - /// /// Test MSI installer repair. /// From 5b32349efccd5dfbea46fa5d476961b94630ffc5 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Thu, 6 Jun 2024 19:17:40 -0700 Subject: [PATCH 10/17] Removed unused includes and test data from project files - In `RepairFlow.h`, the inclusion of `"winget/ManifestCommon.h"` has been removed. - In `AppInstallerCLIE2ETests.csproj`, several test data files, including 'AppInstallerTestMsiInstallerV2.msi` and various YAML files, have been removed --- src/AppInstallerCLICore/Workflows/RepairFlow.h | 1 - .../AppInstallerCLIE2ETests.csproj | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.h b/src/AppInstallerCLICore/Workflows/RepairFlow.h index 0be3bb29c2..08b9a01e11 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.h +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.h @@ -2,7 +2,6 @@ // Licensed under the MIT License. #pragma once #include "ExecutionContext.h" -#include "winget/ManifestCommon.h" namespace AppInstaller::CLI::Workflow { diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index 9cabdf036c..7a724c4a23 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -50,17 +50,8 @@ - - - - - - - - - PreserveNewest @@ -86,9 +77,6 @@ PreserveNewest - - PreserveNewest - From da362f306925d01d84bd74b43af9ffc8ee58a5aa Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Thu, 6 Jun 2024 19:26:34 -0700 Subject: [PATCH 11/17] Removed internal implementation comments from RepairFlow.cpp. --- .../Workflows/RepairFlow.cpp | 61 +------------------ 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index 8f2ffb344a..2e0811d5bb 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -26,20 +26,13 @@ namespace AppInstaller::CLI::Workflow // Internal implementation details namespace { - // Handles Repair behavior Unknown scenario. - // RequiredArgs:None - // Inputs:None - // Outputs:None + void HandleUnknownRepair(Execution::Context& context) { context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } - // Sets the uninstall string in the context. - // RequiredArgs: - // Inputs:InstalledPackageVersion - // Outputs:SilentUninstallString, UninstallString void SetUninstallStringInContext(Execution::Context& context) { const auto& installedPackageVersion = context.Get(); @@ -66,10 +59,6 @@ namespace AppInstaller::CLI::Workflow context.Add(uninstallCommandItr->second); } - // Sets the modify path in the context. - // RequiredArgs:None - // Inputs:InstalledPackageVersion - // Outputs:ModifyPath void SetModifyPathInContext(Execution::Context& context) { const auto& installedPackageVersion = context.Get(); @@ -85,10 +74,6 @@ namespace AppInstaller::CLI::Workflow context.Add(modifyPathItr->second); } - // Sets the product codes in the context. - // RequiredArgs:None - // Inputs:InstalledPackageVersion - // Outputs:ProductCodes void SetProductCodesInContext(Execution::Context& context) { const auto& installedPackageVersion = context.Get(); @@ -102,10 +87,6 @@ namespace AppInstaller::CLI::Workflow context.Add(productCodes); } - // Sets the package family names in the context. - // RequiredArgs:None - // Inputs:InstalledPackageVersion - // Outputs:PackageFamilyNames void SetPackageFamilyNamesInContext(Execution::Context& context) { const auto& installedPackageVersion = context.Get(); @@ -119,10 +100,6 @@ namespace AppInstaller::CLI::Workflow context.Add(packageFamilyNames); } - // Obtains the installer type from the installed package. - // RequiredArgs: None - // Inputs: InstalledPackageVersion - // Outputs: InstallerTypeEnum InstallerTypeEnum GetInstalledPackageInstallerType(Execution::Context& context) { const auto& installedPackage = context.Get(); @@ -130,10 +107,6 @@ namespace AppInstaller::CLI::Workflow return ConvertToInstallerTypeEnum(installedType); } - // The function performs a preliminary check on the installed package by reading its ARP registry flags for NoModify and NoRepair to confirm if the repair operation is applicable. - // RequiredArgs:None - // Inputs:InstalledPackageVersion, NoModify ?, NoRepair ? - // Outputs:None void ApplicabilityCheckForInstalledPackage(Execution::Context& context) { // Installed Package repair applicability check @@ -163,10 +136,6 @@ namespace AppInstaller::CLI::Workflow } } - // This function performs a preliminary check on the available matching package by reading its manifest entries for repair behavior to determine the type of repair operation and repair switch are applicable - // RequiredArgs:None - // Inputs:InstallerType, RepairBehavior - // Outputs:None void ApplicabilityCheckForAvailablePackage(Execution::Context& context) { InstallerTypeEnum installedPackageInstallerType = GetInstalledPackageInstallerType(context); @@ -196,20 +165,12 @@ namespace AppInstaller::CLI::Workflow } } - // Sets the repair string in the context based on the repair behavior and installer type. - // RequiredArgs:None - // Inputs:Installer, RepairBehavior, InstalledPackageVersion, InstallerArgs - // Outputs:RepairString void HandleModifyRepairBehavior(Execution::Context& context, std::string& repairCommand) { SetModifyPathInContext(context); repairCommand += context.Get(); } - // Sets the repair string in the context based on the repair behavior and installer type. - // RequiredArgs:None - // Inputs:Installer, RepairBehavior, InstalledPackageVersion, InstallerArgs - // Outputs:RepairString void HandleInstallerRepairBehavior(Execution::Context& context, InstallerTypeEnum installerType) { context << @@ -226,20 +187,12 @@ namespace AppInstaller::CLI::Workflow } } - // Sets the repair string in the context based on the repair behavior and installer type. - // RequiredArgs:None - // Inputs:Installer, RepairBehavior, InstalledPackageVersion, InstallerArgs - // Outputs:RepairString void HandleUninstallerRepairBehavior(Execution::Context& context, std::string& repairCommand) { SetUninstallStringInContext(context); repairCommand += context.Get(); } - // Generate the repair string based on the repair behavior and installer type. - // RequiredArgs:None - // Inputs:BaseInstallerType, RepairBehavior, ModifyPath?, UninstallString?, InstallerArgs - // Outputs:RepairString void GenerateRepairString(Execution::Context& context) { const auto& installer = context.Get(); @@ -284,10 +237,6 @@ namespace AppInstaller::CLI::Workflow context.Add(repairCommand); } - // Determines if installer mapping is required for the given installed package. - // RequiredArgs: None - // Inputs: InstalledPackageVersion - // Outputs: None bool IsInstallerMappingRequired(Execution::Context& context) { const auto& installedPackage = context.Get(); @@ -306,10 +255,6 @@ namespace AppInstaller::CLI::Workflow } } - // Manages the repair operation for the installed package, specifically for the repair behavior types 'Modify' and 'Uninstall'. - // RequiredArgs: None - // Inputs: RepairBehavior - // Outputs: None void HandleModifyOrUninstallerRepair(Execution::Context& context, RepairBehaviorEnum repairBehavior) { context << @@ -317,10 +262,6 @@ namespace AppInstaller::CLI::Workflow ReportRepairResult(RepairBehaviorToString(repairBehavior), APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED); } - // Manages the repair operation for the installed package, specifically for the repair behavior type 'Installer'. - // RequiredArgs: None - // Inputs: RepairBehavior - // Outputs: None void HandleInstallerRepair(Execution::Context& context, RepairBehaviorEnum repairBehavior) { context << From e918ee9e8c18617ee60ea2853ceacef0b6aeb420 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Thu, 6 Jun 2024 22:39:59 -0700 Subject: [PATCH 12/17] Refactor RepairFlow and update function names - The function `SelectApplicableInstallerWhenEssential` has been renamed to `SelectApplicableInstallerIfNecessary` and modified to include new checks and calls. - The function `HandleUnknownRepair` has been removed and its error handling code has been directly inserted where it was previously called. - The function `GetInstalledPackageInstallerType` has been renamed to `GetInstalledType`. - Comments in `RepairFlow.cpp` and `RepairFlow.h` have been updated to reflect these changes. --- .../Commands/RepairCommand.cpp | 2 +- .../Workflows/RepairFlow.cpp | 61 +++++++++---------- .../Workflows/RepairFlow.h | 2 +- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/RepairCommand.cpp b/src/AppInstallerCLICore/Commands/RepairCommand.cpp index ff992baafa..39543f527d 100644 --- a/src/AppInstallerCLICore/Commands/RepairCommand.cpp +++ b/src/AppInstallerCLICore/Commands/RepairCommand.cpp @@ -117,7 +117,7 @@ namespace AppInstaller::CLI context << Workflow::GetInstalledPackageVersion << - Workflow::SelectApplicableInstallerWhenEssential << + Workflow::SelectApplicableInstallerIfNecessary << Workflow::RepairSinglePackage; } } diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index 2e0811d5bb..0ef17455d4 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -27,12 +27,6 @@ namespace AppInstaller::CLI::Workflow namespace { - void HandleUnknownRepair(Execution::Context& context) - { - context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); - } - void SetUninstallStringInContext(Execution::Context& context) { const auto& installedPackageVersion = context.Get(); @@ -53,7 +47,8 @@ namespace AppInstaller::CLI::Workflow if (uninstallCommandItr == packageMetadata.end()) { - context << HandleUnknownRepair; + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } context.Add(uninstallCommandItr->second); @@ -68,7 +63,8 @@ namespace AppInstaller::CLI::Workflow auto modifyPathItr = packageMetadata.find(PackageVersionMetadata::StandardModifyCommand); if (modifyPathItr == packageMetadata.end()) { - context << HandleUnknownRepair; + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } context.Add(modifyPathItr->second); @@ -81,7 +77,8 @@ namespace AppInstaller::CLI::Workflow if (productCodes.empty()) { - context << HandleUnknownRepair; + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } context.Add(productCodes); @@ -94,13 +91,14 @@ namespace AppInstaller::CLI::Workflow auto packageFamilyNames = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); if (packageFamilyNames.empty()) { - context << HandleUnknownRepair; + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } context.Add(packageFamilyNames); } - InstallerTypeEnum GetInstalledPackageInstallerType(Execution::Context& context) + InstallerTypeEnum GetInstalledType(Execution::Context& context) { const auto& installedPackage = context.Get(); std::string installedType = installedPackage->GetMetadata()[PackageVersionMetadata::InstalledType]; @@ -138,11 +136,8 @@ namespace AppInstaller::CLI::Workflow void ApplicabilityCheckForAvailablePackage(Execution::Context& context) { - InstallerTypeEnum installedPackageInstallerType = GetInstalledPackageInstallerType(context); - // Skip the Available Package applicability check for MSI and MSIX repair as they aren't needed. - if (installedPackageInstallerType == InstallerTypeEnum::Msi - || installedPackageInstallerType == InstallerTypeEnum::Msix) + if (!context.Contains(Execution::Data::Installer)) { return; } @@ -161,7 +156,8 @@ namespace AppInstaller::CLI::Workflow if (DoesInstallerTypeRequireRepairBehaviorForRepair(installerType) && repairBehavior == RepairBehaviorEnum::Unknown) { - context << HandleUnknownRepair; + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } } @@ -214,7 +210,8 @@ namespace AppInstaller::CLI::Workflow break; case RepairBehaviorEnum::Unknown: default: - context << HandleUnknownRepair; + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } context << @@ -229,7 +226,8 @@ namespace AppInstaller::CLI::Workflow if (repairCommand.empty()) { - context << HandleUnknownRepair; + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } repairCommand += " "; @@ -241,7 +239,7 @@ namespace AppInstaller::CLI::Workflow { const auto& installedPackage = context.Get(); std::string installedType = installedPackage->GetMetadata()[PackageVersionMetadata::InstalledType]; - InstallerTypeEnum installerTypeEnum = GetInstalledPackageInstallerType(context); + InstallerTypeEnum installerTypeEnum = GetInstalledType(context); // Installer mapping is not required for MSI and MSIX (Non-store) repair, as we rely on platform support and its dependency on the installed package. if (installerTypeEnum == InstallerTypeEnum::Msi @@ -285,7 +283,8 @@ namespace AppInstaller::CLI::Workflow HandleInstallerRepair(context, repairBehavior); break; default: - HandleUnknownRepair(context); + context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); } } @@ -305,7 +304,7 @@ namespace AppInstaller::CLI::Workflow void ExecuteRepair(Execution::Context& context) { - InstallerTypeEnum installerTypeEnum = GetInstalledPackageInstallerType(context); + InstallerTypeEnum installerTypeEnum = GetInstalledType(context); Synchronization::CrossProcessInstallLock lock; @@ -356,11 +355,11 @@ namespace AppInstaller::CLI::Workflow void GetRepairInfo(Execution::Context& context) { - InstallerTypeEnum installerTypeEnum = GetInstalledPackageInstallerType(context); + InstallerTypeEnum installerTypeEnum = GetInstalledType(context); switch (installerTypeEnum) { - // Exe based installers, for installed package all gets mapped to exe extension. + // Exe based installers, for installed package all gets mapped to exe extension. case InstallerTypeEnum::Burn: case InstallerTypeEnum::Exe: case InstallerTypeEnum::Inno: @@ -476,6 +475,12 @@ namespace AppInstaller::CLI::Workflow void SelectApplicablePackageVersion(Execution::Context& context) { + // If the repair flow is initiated with manifest, then we don't need to select the applicable package version. + if(context.Args.Contains(Args::Type::Manifest)) + { + return; + } + const auto& installedPackage = context.Get(); Utility::Version installedVersion = Utility::Version(installedPackage->GetProperty(PackageVersionProperty::Version)); @@ -506,20 +511,14 @@ namespace AppInstaller::CLI::Workflow context.Args.GetArg(Execution::Args::Type::Channel), false); } - void SelectApplicableInstallerWhenEssential(Execution::Context& context) + void SelectApplicableInstallerIfNecessary(Execution::Context& context) { // For MSI installers, the platform provides built-in support for repair via msiexec, hence no need to select an installer. // Similarly, for MSIX packages that are not from the Microsoft Store, selecting an installer is not required. if (IsInstallerMappingRequired(context)) { - // Non Manifest based repair flow - if (!context.Args.Contains(Args::Type::Manifest)) - { - context << - SelectApplicablePackageVersion; - } - context << + SelectApplicablePackageVersion << SelectInstaller << EnsureApplicableInstaller; } diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.h b/src/AppInstallerCLICore/Workflows/RepairFlow.h index 08b9a01e11..2af1febeb5 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.h +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.h @@ -58,7 +58,7 @@ namespace AppInstaller::CLI::Workflow // RequiredArgs:None // Inputs: Package,InstalledPackageVersion, AvailablePackageVersions // Outputs:Manifest, PackageVersion, Installer - void SelectApplicableInstallerWhenEssential(Execution::Context& context); + void SelectApplicableInstallerIfNecessary(Execution::Context& context); // Perform the repair operation for the single package. // RequiredArgs:None From 0ef11dc5efbf06cb61b5314814f0438b9f85138b Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Fri, 7 Jun 2024 00:33:31 -0700 Subject: [PATCH 13/17] `Refactor SetPackageFamilyNamesInContext usage in RepairFlow.cpp` `Moved the line 'context << SetPackageFamilyNamesInContext;' fRepairMsixPackage. This change ensures that the SetPackageFamilyNamesInContext operation is only executed for non-store MSIX packages, possibly when the package is not from the MSStore.` --- src/AppInstallerCLICore/Workflows/RepairFlow.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index 0ef17455d4..42a809704d 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -380,10 +380,6 @@ namespace AppInstaller::CLI::Workflow // MSIX based installers, msix, msstore. case InstallerTypeEnum::Msix: case InstallerTypeEnum::MSStore: - { - context << - SetPackageFamilyNamesInContext; - } break; case InstallerTypeEnum::Portable: default: @@ -408,6 +404,7 @@ namespace AppInstaller::CLI::Workflow else { context << + SetPackageFamilyNamesInContext << RepairMsixNonStorePackage; } } From 6fd832b6e6903017df2d6a62e0f81613bc24cad2 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Mon, 10 Jun 2024 12:07:12 -0700 Subject: [PATCH 14/17] Updated repair functions and improved code readability - Updated the `ExecuteRepair` and `GetRepairInfo` functions to handle different installer types more flexibly - Removed `RepairMsixPackage` function. Updated function declarations in `RepairFlow.h` to reflect these changes. - Simplified the determination of `repairPackage` in `ReportRepairResult`. --- .../Workflows/RepairFlow.cpp | 54 +++++++------------ .../Workflows/RepairFlow.h | 6 --- 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index 42a809704d..53e17b67c9 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -304,7 +304,7 @@ namespace AppInstaller::CLI::Workflow void ExecuteRepair(Execution::Context& context) { - InstallerTypeEnum installerTypeEnum = GetInstalledType(context); + InstallerTypeEnum installerTypeEnum = context.Contains(Execution::Data::Installer) ? context.Get()->EffectiveInstallerType() : GetInstalledType(context); Synchronization::CrossProcessInstallLock lock; @@ -341,10 +341,16 @@ namespace AppInstaller::CLI::Workflow } break; case InstallerTypeEnum::Msix: + { + context << + RepairMsixNonStorePackage; + } + break; case InstallerTypeEnum::MSStore: { context << - RepairMsixPackage; + EnsureStorePolicySatisfied << + MSStoreRepair; } break; case InstallerTypeEnum::Portable: @@ -355,11 +361,11 @@ namespace AppInstaller::CLI::Workflow void GetRepairInfo(Execution::Context& context) { - InstallerTypeEnum installerTypeEnum = GetInstalledType(context); + InstallerTypeEnum installerTypeEnum = context.Contains(Execution::Data::Installer) ? context.Get()->BaseInstallerType : GetInstalledType(context); switch (installerTypeEnum) { - // Exe based installers, for installed package all gets mapped to exe extension. + // Exe based installers, for installed package all gets mapped to exe extension. case InstallerTypeEnum::Burn: case InstallerTypeEnum::Exe: case InstallerTypeEnum::Inno: @@ -377,44 +383,20 @@ namespace AppInstaller::CLI::Workflow SetProductCodesInContext; } break; - // MSIX based installers, msix, msstore. + // MSIX based installers, msix. case InstallerTypeEnum::Msix: + { + context << + SetPackageFamilyNamesInContext; + } case InstallerTypeEnum::MSStore: - break; + break; case InstallerTypeEnum::Portable: default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } } - void RepairMsixPackage(Execution::Context& context) - { - const auto& installedPackage = context.Get(); - std::string installedType = installedPackage->GetMetadata()[PackageVersionMetadata::InstalledType]; - - if (ConvertToInstallerTypeEnum(installedType) == InstallerTypeEnum::Msix) - { - // If the installed package is from Microsoft Store, then we attempt to repair it using MSStoreRepair else do package re-registration. - if (installedPackage->GetSource() == WellKnownSource::MicrosoftStore) - { - context << - EnsureStorePolicySatisfied << - MSStoreRepair; - } - else - { - context << - SetPackageFamilyNamesInContext << - RepairMsixNonStorePackage; - } - } - else - { - // This should never happen as the installer type should be one of the above. - THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); - } - } - void RepairMsixNonStorePackage(Execution::Context& context) { bool isMachineScope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)) == Manifest::ScopeEnum::Machine; @@ -473,7 +455,7 @@ namespace AppInstaller::CLI::Workflow void SelectApplicablePackageVersion(Execution::Context& context) { // If the repair flow is initiated with manifest, then we don't need to select the applicable package version. - if(context.Args.Contains(Args::Type::Manifest)) + if (context.Args.Contains(Args::Type::Manifest)) { return; } @@ -527,7 +509,7 @@ namespace AppInstaller::CLI::Workflow if (repairResult != 0) { - auto& repairPackage = IsInstallerMappingRequired(context) ? + auto& repairPackage = context.Contains(Execution::Data::PackageVersion) ? context.Get() : context.Get(); diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.h b/src/AppInstallerCLICore/Workflows/RepairFlow.h index 2af1febeb5..ff1945b43d 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.h +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.h @@ -35,12 +35,6 @@ namespace AppInstaller::CLI::Workflow // Outputs:RepairString?, ProductCodes?, PackageFamilyNames? void GetRepairInfo(Execution::Context& context); - // Perform the repair operation for the MSIX package. - // RequiredArgs:None - // Inputs:PackageFamilyNames , InstallScope? - // Outputs:None - void RepairMsixPackage(Execution::Context& context); - // Perform the repair operation for the MSIX NonStore package. // RequiredArgs:None // Inputs:PackageFamilyNames , InstallScope? From f65bb709e52ce86b98702c37af86559d193cf9b2 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Mon, 10 Jun 2024 12:15:22 -0700 Subject: [PATCH 15/17] Renamed `RepairMsixNonStorePackage` to `RepairMsixPackage` Renamed the function `RepairMsixNonStorePackage` to `RepairMsixPackage` in `RepairFlow.cpp` and `RepairFlow.h` files. This change is also reflected in the context where this function is called in the `InstallerTypeEnum::Msix` case. --- src/AppInstallerCLICore/Workflows/RepairFlow.cpp | 4 ++-- src/AppInstallerCLICore/Workflows/RepairFlow.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index 53e17b67c9..8db29dd80c 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -343,7 +343,7 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Msix: { context << - RepairMsixNonStorePackage; + RepairMsixPackage; } break; case InstallerTypeEnum::MSStore: @@ -397,7 +397,7 @@ namespace AppInstaller::CLI::Workflow } } - void RepairMsixNonStorePackage(Execution::Context& context) + void RepairMsixPackage(Execution::Context& context) { bool isMachineScope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)) == Manifest::ScopeEnum::Machine; diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.h b/src/AppInstallerCLICore/Workflows/RepairFlow.h index ff1945b43d..d4054f5ab2 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.h +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.h @@ -39,7 +39,7 @@ namespace AppInstaller::CLI::Workflow // RequiredArgs:None // Inputs:PackageFamilyNames , InstallScope? // Outputs:None - void RepairMsixNonStorePackage(Execution::Context& context); + void RepairMsixPackage(Execution::Context& context); // Select the applicable package version by matching the installed package version with the available package version. // RequiredArgs:None From de203b48d94a730afdb553c28bba666e90e79c94 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Mon, 10 Jun 2024 18:39:02 -0700 Subject: [PATCH 16/17] `Refactor msix /msi installer mapping requirment logic in RepairFlow.cpp` - The `IsInstallerMappingRequired` function now uses a `switch` statement for different `InstallerTypeEnum` values and checks remotepackage source for MSIX type to confirm MSStore package check - `SelectApplicablePackageVersion` function now returns early for `Msi` types and `Msix` packages not from the Microsoft Store. - The `SelectApplicableInstallerIfNecessary` function has been updated to select the applicable package version before checking if installer mapping is required. --- .../Workflows/RepairFlow.cpp | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index 8db29dd80c..de15ffbcd4 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -237,18 +237,15 @@ namespace AppInstaller::CLI::Workflow bool IsInstallerMappingRequired(Execution::Context& context) { - const auto& installedPackage = context.Get(); - std::string installedType = installedPackage->GetMetadata()[PackageVersionMetadata::InstalledType]; InstallerTypeEnum installerTypeEnum = GetInstalledType(context); - // Installer mapping is not required for MSI and MSIX (Non-store) repair, as we rely on platform support and its dependency on the installed package. - if (installerTypeEnum == InstallerTypeEnum::Msi - || (installerTypeEnum == InstallerTypeEnum::Msix && installedPackage->GetSource() != WellKnownSource::MicrosoftStore)) + switch (installerTypeEnum) { + case InstallerTypeEnum::Msi: return false; - } - else - { + case InstallerTypeEnum::Msix: + return context.Contains(Execution::Data::PackageVersion) && context.Get()->GetSource().IsWellKnownSource(WellKnownSource::MicrosoftStore); + default: return true; } } @@ -462,6 +459,14 @@ namespace AppInstaller::CLI::Workflow const auto& installedPackage = context.Get(); + InstallerTypeEnum installerTypeEnum = GetInstalledType(context); + + // We don't need to select the applicable package version for MSI installers. + if (installerTypeEnum == InstallerTypeEnum::Msi) + { + return; + } + Utility::Version installedVersion = Utility::Version(installedPackage->GetProperty(PackageVersionProperty::Version)); if (installedVersion.IsUnknown()) { @@ -484,6 +489,18 @@ namespace AppInstaller::CLI::Workflow } } + // For MSIX packages that are not from the Microsoft Store, selecting an installer is not required. + if (installerTypeEnum == InstallerTypeEnum::Msix) + { + PackageVersionKey key("", requestedVersion, context.Args.GetArg(Execution::Args::Type::Channel)); + auto packageVersion = packageVersions->GetVersion(key); + + if (!packageVersion || !packageVersion->GetSource().IsWellKnownSource(WellKnownSource::MicrosoftStore)) + { + return; + } + } + context << GetManifestWithVersionFromPackage( requestedVersion, @@ -492,12 +509,14 @@ namespace AppInstaller::CLI::Workflow void SelectApplicableInstallerIfNecessary(Execution::Context& context) { + context << + SelectApplicablePackageVersion; + // For MSI installers, the platform provides built-in support for repair via msiexec, hence no need to select an installer. // Similarly, for MSIX packages that are not from the Microsoft Store, selecting an installer is not required. if (IsInstallerMappingRequired(context)) { context << - SelectApplicablePackageVersion << SelectInstaller << EnsureApplicableInstaller; } From cd6c65e9186602aaf295305f4b93a216ff55d949 Mon Sep 17 00:00:00 2001 From: Madhusudhan Gumbalapura Sudarshan Date: Fri, 14 Jun 2024 19:32:27 -0700 Subject: [PATCH 17/17] PR Feedback Fix : Refine MSIX handling & simplify installer checks - Refined the logic for MSIX package handling in the repair flow, focusing on Microsoft Store packages and improving installer selection accuracy. - Simplified installer type checks by removing redundant MSI checks, streamlining the repair process. - Adjusted repair command tests for clarity, removing unnecessary string interpolation. - Enhanced documentation and comments for better clarity on the repair flow and installer selection logic. --- .../Workflows/RepairFlow.cpp | 38 +++++++------------ .../Workflows/RepairFlow.h | 2 +- src/AppInstallerCLIE2ETests/RepairCommand.cs | 6 +-- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index de15ffbcd4..9f113de15b 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -244,7 +244,19 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Msi: return false; case InstallerTypeEnum::Msix: - return context.Contains(Execution::Data::PackageVersion) && context.Get()->GetSource().IsWellKnownSource(WellKnownSource::MicrosoftStore); + // For MSIX packages that are from the Microsoft Store, selecting an installer is required. + if (context.Contains(Execution::Data::Package)) + { + auto availablePackages = context.Get()->GetAvailable(); + + if (availablePackages.size() == 1 && availablePackages[0]->GetSource() == WellKnownSource::MicrosoftStore) + { + return true; + } + } + + // For MSIX packages that are not from the Microsoft Store, selecting an installer is not required. + return false; default: return true; } @@ -459,14 +471,6 @@ namespace AppInstaller::CLI::Workflow const auto& installedPackage = context.Get(); - InstallerTypeEnum installerTypeEnum = GetInstalledType(context); - - // We don't need to select the applicable package version for MSI installers. - if (installerTypeEnum == InstallerTypeEnum::Msi) - { - return; - } - Utility::Version installedVersion = Utility::Version(installedPackage->GetProperty(PackageVersionProperty::Version)); if (installedVersion.IsUnknown()) { @@ -489,18 +493,6 @@ namespace AppInstaller::CLI::Workflow } } - // For MSIX packages that are not from the Microsoft Store, selecting an installer is not required. - if (installerTypeEnum == InstallerTypeEnum::Msix) - { - PackageVersionKey key("", requestedVersion, context.Args.GetArg(Execution::Args::Type::Channel)); - auto packageVersion = packageVersions->GetVersion(key); - - if (!packageVersion || !packageVersion->GetSource().IsWellKnownSource(WellKnownSource::MicrosoftStore)) - { - return; - } - } - context << GetManifestWithVersionFromPackage( requestedVersion, @@ -509,14 +501,12 @@ namespace AppInstaller::CLI::Workflow void SelectApplicableInstallerIfNecessary(Execution::Context& context) { - context << - SelectApplicablePackageVersion; - // For MSI installers, the platform provides built-in support for repair via msiexec, hence no need to select an installer. // Similarly, for MSIX packages that are not from the Microsoft Store, selecting an installer is not required. if (IsInstallerMappingRequired(context)) { context << + SelectApplicablePackageVersion << SelectInstaller << EnsureApplicableInstaller; } diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.h b/src/AppInstallerCLICore/Workflows/RepairFlow.h index d4054f5ab2..5c15d9f666 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.h +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.h @@ -48,7 +48,7 @@ namespace AppInstaller::CLI::Workflow void SelectApplicablePackageVersion(Execution::Context& context); /// - /// Select the applicable installer for the installed package is essential. + /// Select the applicable installer for the installed package if necessary. // RequiredArgs:None // Inputs: Package,InstalledPackageVersion, AvailablePackageVersions // Outputs:Manifest, PackageVersion, Installer diff --git a/src/AppInstallerCLIE2ETests/RepairCommand.cs b/src/AppInstallerCLIE2ETests/RepairCommand.cs index 289a0e0ee4..53c361ad72 100644 --- a/src/AppInstallerCLIE2ETests/RepairCommand.cs +++ b/src/AppInstallerCLIE2ETests/RepairCommand.cs @@ -49,7 +49,7 @@ public void RepairMSIInstaller() // This would allow the 'msiexec repair' command to function as expected. string installerSourceDir = TestCommon.CopyInstallerFileToARPInstallSourceDirectory(TestCommon.GetTestDataFile("AppInstallerTestMsiInstallerV2.msi"), Constants.MsiInstallerProductCode, true); - result = TestCommon.RunAICLICommand("repair", $"AppInstallerTest.TestMsiRepair"); + result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestMsiRepair"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Repair operation completed successfully")); Assert.True(TestCommon.VerifyTestMsiInstalledAndCleanup(installDir)); @@ -87,7 +87,7 @@ public void RepairNonStoreMsixPackageWithMachineScope() { // Selecting Microsoft.Paint_8wekyb3d8bbwe because it's a system package suitable for this scenario. // First, we need to ensure this package is installed, otherwise, we skip the test. - var result = TestCommon.RunAICLICommand("list", $"Microsoft.Paint_8wekyb3d8bbwe"); + var result = TestCommon.RunAICLICommand("list", "Microsoft.Paint_8wekyb3d8bbwe"); if (result.ExitCode != Constants.ErrorCode.S_OK) { @@ -96,7 +96,7 @@ public void RepairNonStoreMsixPackageWithMachineScope() Assert.True(result.StdOut.Contains("Microsoft.Paint_8wekyb3d8bbwe")); - result = TestCommon.RunAICLICommand("repair", $"Microsoft.Paint_8wekyb3d8bbwe --scope machine"); + result = TestCommon.RunAICLICommand("repair", "Microsoft.Paint_8wekyb3d8bbwe --scope machine"); Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALL_SYSTEM_NOT_SUPPORTED, result.ExitCode); Assert.True(result.StdOut.Contains("The current system configuration does not support the repair of this package.")); }