Skip to content

Commit

Permalink
Repair-WinGetPackageManager improvements (#3423)
Browse files Browse the repository at this point in the history
This PR address #3374

Change
Make Repair-WinGetPackageManager repair known issues until its fixed or there's a non fixable state.
Adds support for installing Microsoft.UI.Xaml.2.7 package from their GitHub release.
Adds new parameter switch -AllUsers to Repair-WinGetPackageManager. If this is on, repair uses Add-AppxProvisionedPackage instead of Add-AppxPackage.
Adds new integrity category to detect winget.exe failures due to missing license. To fix it Repair-WinGetPackageManager -AllUsers must be executed in admin mode.
Fix adding preprocessor macros for net48. This cause Repair-WinGetPackageManager to always fail for Windows PowerShell.
There's a breaking change in Repair-WinGetPackageManager. It will now throw if there's an issue repairing. Good thing this is a "prerelease" module 🗡️
Validation
Test locally on machines where Microsoft.UI.Xaml.2.7 was not preinstalled and on Windows Server 2022.
  • Loading branch information
msftrubengu committed Jul 14, 2023
1 parent 2287ad2 commit d637f0e
Show file tree
Hide file tree
Showing 14 changed files with 607 additions and 248 deletions.
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ UWP
VALUENAMECASE
VERSI
VERSIE
vclib
vns
vsconfig
vstest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ namespace Microsoft.WinGet.Client.Commands
[OutputType(typeof(int))]
public class RepairWinGetPackageManagerCmdlet : WinGetPackageManagerCmdlet
{
/// <summary>
/// Gets or sets a value indicating whether to repair for all users. Requires admin.
/// </summary>
[Parameter(ValueFromPipelineByPropertyName = true)]
public SwitchParameter AllUsers { get; set; }

/// <summary>
/// Attempts to repair winget.
/// TODO: consider WhatIf and Confirm options.
Expand All @@ -30,11 +36,11 @@ protected override void ProcessRecord()
var command = new WinGetPackageManagerCommand(this);
if (this.ParameterSetName == Constants.IntegrityLatestSet)
{
command.RepairUsingLatest(this.IncludePreRelease.ToBool());
command.RepairUsingLatest(this.IncludePreRelease.ToBool(), this.AllUsers.ToBool());
}
else
{
command.Repair(this.Version);
command.Repair(this.Version, this.AllUsers.ToBool());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,21 @@
namespace Microsoft.WinGet.Client.Engine.Commands
{
using System;
using System.Collections.Generic;
using System.Management.Automation;
using Microsoft.WinGet.Client.Engine.Commands.Common;
using Microsoft.WinGet.Client.Engine.Common;
using Microsoft.WinGet.Client.Engine.Exceptions;
using Microsoft.WinGet.Client.Engine.Helpers;
using Microsoft.WinGet.Client.Engine.Properties;
using static Microsoft.WinGet.Client.Engine.Common.Constants;

/// <summary>
/// Used by Repair-WinGetPackageManager and Assert-WinGetPackageManager.
/// </summary>
public sealed class WinGetPackageManagerCommand : BaseCommand
{
private const string EnvPath = "env:PATH";
private const int Succeeded = 0;
private const int Failed = -1;

private static readonly string[] WriteInformationTags = new string[] { "PSHOST" };

/// <summary>
/// Initializes a new instance of the <see cref="WinGetPackageManagerCommand"/> class.
Expand All @@ -39,8 +38,8 @@ public WinGetPackageManagerCommand(PSCmdlet psCmdlet)
/// <param name="preRelease">Use prerelease version on GitHub.</param>
public void AssertUsingLatest(bool preRelease)
{
var gitHubRelease = new GitHubRelease();
string expectedVersion = gitHubRelease.GetLatestVersionTagName(preRelease);
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
string expectedVersion = gitHubClient.GetLatestVersionTagName(preRelease);
this.Assert(expectedVersion);
}

Expand All @@ -57,144 +56,132 @@ public void Assert(string expectedVersion)
/// Repairs winget using the latest version on winget-cli.
/// </summary>
/// <param name="preRelease">Use prerelease version on GitHub.</param>
public void RepairUsingLatest(bool preRelease)
/// <param name="allUsers">Install for all users. Requires admin.</param>
public void RepairUsingLatest(bool preRelease, bool allUsers)
{
var gitHubRelease = new GitHubRelease();
string expectedVersion = gitHubRelease.GetLatestVersionTagName(preRelease);
this.Repair(expectedVersion);
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
string expectedVersion = gitHubClient.GetLatestVersionTagName(preRelease);
this.Repair(expectedVersion, allUsers);
}

/// <summary>
/// Repairs winget if needed.
/// </summary>
/// <param name="expectedVersion">The expected version, if any.</param>
public void Repair(string expectedVersion)
/// <param name="allUsers">Install for all users. Requires admin.</param>
public void Repair(string expectedVersion, bool allUsers)
{
int result = Failed;

var integrityCategory = WinGetIntegrity.GetIntegrityCategory(this.PsCmdlet, expectedVersion);
this.PsCmdlet.WriteDebug($"Integrity category type: {integrityCategory}");

if (integrityCategory == IntegrityCategory.Installed ||
integrityCategory == IntegrityCategory.UnexpectedVersion)
{
result = this.VerifyWinGetInstall(integrityCategory, expectedVersion);
}
else if (integrityCategory == IntegrityCategory.NotInPath)
{
this.RepairEnvPath();

// Now try again and get the desired winget version if needed.
var newIntegrityCategory = WinGetIntegrity.GetIntegrityCategory(this.PsCmdlet, expectedVersion);
this.PsCmdlet.WriteDebug($"Integrity category after fixing PATH {newIntegrityCategory}");
result = this.VerifyWinGetInstall(newIntegrityCategory, expectedVersion);
}
else if (integrityCategory == IntegrityCategory.AppInstallerNotRegistered)
if (allUsers)
{
var appxModule = new AppxModuleHelper(this.PsCmdlet);
appxModule.RegisterAppInstaller();

// Now try again and get the desired winget version if needed.
var newIntegrityCategory = WinGetIntegrity.GetIntegrityCategory(this.PsCmdlet, expectedVersion);
this.PsCmdlet.WriteDebug($"Integrity category after registering {newIntegrityCategory}");
result = this.VerifyWinGetInstall(newIntegrityCategory, expectedVersion);
}
else if (integrityCategory == IntegrityCategory.AppInstallerNotInstalled ||
integrityCategory == IntegrityCategory.AppInstallerNotSupported ||
integrityCategory == IntegrityCategory.Failure)
{
// If we are here and expectedVersion is empty, it means that they just ran Repair-WinGetPackageManager.
// When there is not version specified, we don't want to assume an empty version means latest, but in
// this particular case we need to.
if (string.IsNullOrEmpty(expectedVersion))
if (Utilities.ExecutingAsSystem)
{
var gitHubRelease = new GitHubRelease();
expectedVersion = gitHubRelease.GetLatestVersionTagName(false);
throw new NotSupportedException();
}

if (this.DownloadAndInstall(expectedVersion, false))
{
result = Succeeded;
}
else
if (!Utilities.ExecutingAsAdministrator)
{
this.PsCmdlet.WriteDebug($"Failed installing {expectedVersion}");
throw new WinGetRepairException(Resources.RepairAllUsersMessage);
}
}
else if (integrityCategory == IntegrityCategory.AppExecutionAliasDisabled)
{
// Sorry, but the user has to manually enabled it.
this.PsCmdlet.WriteInformation(Resources.AppExecutionAliasDisabledHelpMessage, WriteInformationTags);
}
else
{
this.PsCmdlet.WriteInformation(Resources.WinGetNotSupportedMessage, WriteInformationTags);
}

this.PsCmdlet.WriteObject(result);
this.RepairStateMachine(expectedVersion, allUsers);
}

private int VerifyWinGetInstall(IntegrityCategory integrityCategory, string expectedVersion)
private void RepairStateMachine(string expectedVersion, bool allUsers)
{
if (integrityCategory == IntegrityCategory.Installed)
{
// Nothing to do
this.PsCmdlet.WriteDebug($"WinGet is in a good state.");
return Succeeded;
}
else if (integrityCategory == IntegrityCategory.UnexpectedVersion)
var seenCategories = new HashSet<IntegrityCategory>();

var currentCategory = IntegrityCategory.Unknown;
while (currentCategory != IntegrityCategory.Installed)
{
// The versions are different, download and install.
if (!this.InstallDifferentVersion(new WinGetVersion(expectedVersion)))
try
{
this.PsCmdlet.WriteDebug($"Failed installing {expectedVersion}");
WinGetIntegrity.AssertWinGet(this.PsCmdlet, expectedVersion);
this.PsCmdlet.WriteDebug($"WinGet is in a good state.");
currentCategory = IntegrityCategory.Installed;
}
else
catch (WinGetIntegrityException e)
{
return Succeeded;
currentCategory = e.Category;

if (seenCategories.Contains(currentCategory))
{
this.PsCmdlet.WriteDebug($"{currentCategory} encountered previously");
throw;
}

this.PsCmdlet.WriteDebug($"Integrity category type: {currentCategory}");
seenCategories.Add(currentCategory);

switch (currentCategory)
{
case IntegrityCategory.UnexpectedVersion:
this.InstallDifferentVersion(new WinGetVersion(expectedVersion), allUsers);
break;
case IntegrityCategory.NotInPath:
this.RepairEnvPath();
break;
case IntegrityCategory.AppInstallerNotRegistered:
this.Register();
break;
case IntegrityCategory.AppInstallerNotInstalled:
case IntegrityCategory.AppInstallerNotSupported:
case IntegrityCategory.Failure:
this.Install(expectedVersion, allUsers);
break;
case IntegrityCategory.AppInstallerNoLicense:
// This requires -AllUsers in admin mode.
if (allUsers && Utilities.ExecutingAsAdministrator)
{
this.Install(expectedVersion, allUsers);
}
else
{
throw new WinGetRepairException(e);
}

break;
case IntegrityCategory.AppExecutionAliasDisabled:
case IntegrityCategory.Unknown:
throw new WinGetRepairException(e);
default:
throw new NotSupportedException();
}
}
}

return Failed;
}

private bool InstallDifferentVersion(WinGetVersion toInstallVersion)
private void InstallDifferentVersion(WinGetVersion toInstallVersion, bool allUsers)
{
var installedVersion = WinGetVersion.InstalledWinGetVersion;
bool isDowngrade = installedVersion.CompareAsDeployment(toInstallVersion) > 0;

this.PsCmdlet.WriteDebug($"Installed WinGet version {installedVersion.TagVersion}");
this.PsCmdlet.WriteDebug($"Installing WinGet version {toInstallVersion.TagVersion}");
this.PsCmdlet.WriteDebug($"Installed WinGet version '{installedVersion.TagVersion}' " +
$"Installing WinGet version '{toInstallVersion.TagVersion}' " +
$"Is downgrade {isDowngrade}");
var appxModule = new AppxModuleHelper(this.PsCmdlet);
appxModule.InstallFromGitHubRelease(toInstallVersion.TagVersion, allUsers, isDowngrade);
}

bool downgrade = false;
if (installedVersion.CompareAsDeployment(toInstallVersion) > 0)
private void Install(string toInstallVersion, bool allUsers)
{
// If we are here and toInstallVersion is empty, it means that they just ran Repair-WinGetPackageManager.
// When there is not version specified, we don't want to assume an empty version means latest, but in
// this particular case we need to.
if (string.IsNullOrEmpty(toInstallVersion))
{
downgrade = true;
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
toInstallVersion = gitHubClient.GetLatestVersionTagName(false);
}

return this.DownloadAndInstall(toInstallVersion.TagVersion, downgrade);
var appxModule = new AppxModuleHelper(this.PsCmdlet);
appxModule.InstallFromGitHubRelease(toInstallVersion, allUsers, false);
}

private bool DownloadAndInstall(string versionTag, bool downgrade)
private void Register()
{
using var tempFile = new TempFile();

// Download and install.
var gitHubRelease = new GitHubRelease();
gitHubRelease.DownloadRelease(versionTag, tempFile.FullPath);

var appxModule = new AppxModuleHelper(this.PsCmdlet);
appxModule.AddAppInstallerBundle(tempFile.FullPath, downgrade);

// Verify that is installed
var integrityCategory = WinGetIntegrity.GetIntegrityCategory(this.PsCmdlet, versionTag);
if (integrityCategory != IntegrityCategory.Installed)
{
this.PsCmdlet.WriteDebug($"Failed installing {versionTag}. IntegrityCategory after attempt: '{integrityCategory}'");
return false;
}

this.PsCmdlet.WriteDebug($"Installed WinGet version {versionTag}");
return true;
appxModule.RegisterAppInstaller();
}

private void RepairEnvPath()
Expand Down
27 changes: 27 additions & 0 deletions src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,32 @@ internal static class Constants
/// Name of PATH environment variable.
/// </summary>
public const string PathEnvVar = "PATH";

/// <summary>
/// Repository owners.
/// </summary>
public class RepositoryOwner
{
/// <summary>
/// Microsoft org.
/// </summary>
public const string Microsoft = "microsoft";
}

/// <summary>
/// Repository names.
/// </summary>
public class RepositoryName
{
/// <summary>
/// https://github.com/microsoft/winget-cli .
/// </summary>
public const string WinGetCli = "winget-cli";

/// <summary>
/// https://github.com/microsoft/microsoft-ui-xaml .
/// </summary>
public const string UiXaml = "microsoft-ui-xaml";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,10 @@ public enum IntegrityCategory
/// Installed App Installer package is not supported.
/// </summary>
AppInstallerNotSupported,

/// <summary>
/// No applicable license found.
/// </summary>
AppInstallerNoLicense,
}
}
Loading

0 comments on commit d637f0e

Please sign in to comment.