Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repair-WinGetPackageManager improvements #3423

Merged
merged 10 commits into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,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
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
Loading