Skip to content

Commit

Permalink
Winget Repair - Eliminate installer type mapping for MSI/WIX and MSIX…
Browse files Browse the repository at this point in the history
… NonStore ,code refactoring & E2E Test Coverage (#4534)

**Repair Command updates:**
- 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.

**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

**E2E Test Coverage:**
- 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.

**How Validated:**
- Configured LocalhostWebServer for local execution. 
- Executed WingetRepair Tests via Visual Studio TestExplorer, confirming
all tests passed on the local machine


![image](https://github.com/microsoft/winget-cli/assets/53235553/e3b935b6-4be3-419e-a81d-dd639f5d9377)

<!-- To check a checkbox place an "x" between the brackets. e.g: [x] -->

- [x] I have signed the [Contributor License
Agreement](https://cla.opensource.microsoft.com/microsoft/winget-pkgs).
- [ ] This pull request is related to an issue.

-----

###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4534)
  • Loading branch information
Madhusudhan-MSFT authored Jun 15, 2024
1 parent f190823 commit 04b2c4c
Show file tree
Hide file tree
Showing 20 changed files with 972 additions and 118 deletions.
16 changes: 7 additions & 9 deletions src/AppInstallerCLICore/Commands/RepairCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,20 @@ 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
{
context <<
Workflow::SearchSourceForSingle <<
Workflow::HandleSearchResultFailures <<
Workflow::EnsureOneMatchFromSearchResult(OperationType::Repair) <<
Workflow::ReportPackageIdentity <<
Workflow::GetInstalledPackageVersion <<
Workflow::SelectApplicablePackageVersion <<
Workflow::RepairSinglePackage;
Workflow::ReportPackageIdentity;
}

context <<
Workflow::GetInstalledPackageVersion <<
Workflow::SelectApplicableInstallerIfNecessary <<
Workflow::RepairSinglePackage;
}
}
222 changes: 136 additions & 86 deletions src/AppInstallerCLICore/Workflows/RepairFlow.cpp

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion src/AppInstallerCLICore/Workflows/RepairFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ namespace AppInstaller::CLI::Workflow
// Outputs:RepairString?, ProductCodes?, PackageFamilyNames?
void GetRepairInfo(Execution::Context& context);

// Perform the repair operation for the MSIX package.
// Perform the repair operation for the MSIX NonStore package.
// RequiredArgs:None
// Inputs:PackageFamilyNames , InstallScope?
// Outputs:None
Expand All @@ -47,6 +47,13 @@ namespace AppInstaller::CLI::Workflow
// Outputs:Manifest, PackageVersion, Installer
void SelectApplicablePackageVersion(Execution::Context& context);

/// <summary>
/// Select the applicable installer for the installed package if necessary.
// RequiredArgs:None
// Inputs: Package,InstalledPackageVersion, AvailablePackageVersions
// Outputs:Manifest, PackageVersion, Installer
void SelectApplicableInstallerIfNecessary(Execution::Context& context);

// Perform the repair operation for the single package.
// RequiredArgs:None
// Inputs: SearchResult, InstalledPackage, ApplicableInstaller
Expand Down
7 changes: 7 additions & 0 deletions src/AppInstallerCLIE2ETests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -263,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);
Expand Down
131 changes: 126 additions & 5 deletions src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -470,6 +471,32 @@ public static bool VerifyTestExeInstalled(string installDir, string expectedCont
return verifyInstallSuccess;
}

/// <summary>
/// Verifies if the repair of the test executable was successful.
/// </summary>
/// <param name="installDir">The directory where the test executable is installed.</param>
/// <param name="expectedContent">The expected content in the test executable file. This is optional.</param>
/// <returns>Returns true if the repair was successful, false otherwise.</returns>
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;
}

/// <summary>
/// Verify installer and manifest downloaded correctly and cleanup.
/// </summary>
Expand Down Expand Up @@ -543,6 +570,35 @@ public static bool VerifyInstallerDownload(
return downloadResult;
}

/// <summary>
/// Best effort test exe cleanup.
/// </summary>
/// <param name="installDir">Install directory.</param>
public static void BestEffortTestExeCleanup(string installDir)
{
var uninstallerPath = Path.Combine(installDir, Constants.TestExeUninstallerFileName);
if (File.Exists(uninstallerPath))
{
RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName));
}
}

/// <summary>
/// Best effort test exe cleanup and install directory cleanup.
/// </summary>
/// <param name="installDir">Install directory.</param>
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);
}
}

/// <summary>
/// Verify exe installer correctly and then uninstall it.
/// </summary>
Expand All @@ -554,15 +610,25 @@ 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;
}

/// <summary>
/// Verify exe repair completed and cleanup.
/// </summary>
/// <param name="installDir">Install directory.</param>
/// <param name="expectedContent">Optional expected context.</param>
/// <returns>True if success.</returns>
public static bool VerifyTestExeRepairCompletedAndCleanup(string installDir, string expectedContent = null)
{
bool verifyRepairSuccess = VerifyTestExeRepairSuccessful(installDir, expectedContent);
CleanupTestExeAndDirectory(installDir);

return verifyRepairSuccess;
}

/// <summary>
/// Verify msi installed correctly.
/// </summary>
Expand Down Expand Up @@ -912,6 +978,61 @@ public static string GetExpectedModulePath(TestModuleLocation location)
}
}

/// <summary>
/// Copy the installer file to the ARP InstallSource directory.
/// </summary>
/// <param name="installerFilePath">Test installer to be copied.</param>
/// <param name="productCode">Installer Product.</param>
/// <param name="useWoW6432Node">is WoW6432Node to use.</param>
/// <returns>Returns the installer source directory if the file operation is successful, otherwise returns an empty string.</returns>
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;
}

/// <summary>
/// Run winget command via direct process.
/// </summary>
Expand Down
25 changes: 24 additions & 1 deletion src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// <copyright file="TestIndex.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
Expand All @@ -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);
}
Expand All @@ -36,6 +37,11 @@ static TestIndex()
/// </summary>
public static string MsiInstaller { get; private set; }

/// <summary>
/// Gets the signed msi installerV2 path used by the manifests in the E2E test.
/// </summary>
public static string MsiInstallerV2 { get; private set; }

/// <summary>
/// Gets the signed msix installer path used by the manifests in the E2E test.
/// </summary>
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -118,6 +134,13 @@ public static void GenerateE2ESource()
HashToken = "<MSIHASH>",
},
new LocalInstaller
{
Type = InstallerType.Msi,
Name = Path.Combine(Constants.MsiInstaller, Constants.MsiInstallerV2FileName),
Input = testParams.MsiInstallerPath,
HashToken = "<MSIHASHV2>",
},
new LocalInstaller
{
Type = InstallerType.Msix,
Name = Path.Combine(Constants.MsixInstaller, Constants.MsixInstallerFileName),
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -103,6 +104,11 @@ public static TestSetup Parameters
/// </summary>
public string MsiInstallerPath { get; }

/// <summary>
/// Gets the msi installer V2 path.
/// </summary>
public string MsiInstallerV2Path { get; }

/// <summary>
/// Gets the msix installer path.
/// </summary>
Expand Down
Loading

0 comments on commit 04b2c4c

Please sign in to comment.