Skip to content

Commit

Permalink
Implement PowerShell cmdlets (#2838)
Browse files Browse the repository at this point in the history
This PR implements two new PowerShelll cmdlets Assert-WinGetPackageManager and Repair-WinGetPackageManager as well as the WinGetPackageManager DSC Resource.

Assert-WinGetPackageManager verifies the integrity of winget for the current user. If winget is not set up correctly, it will throw an exception with the type of failure if detected.
It can detect:

Failed call into winget --version.
winget is not found because is not in the PATH environment variable.
winget is not found because the App execution alias is disabled.
Windows OS is not supported.
The AppInstaller package is not installed in the machine.
The AppInstaller package is not registered for the user.
The AppInstaller package is old and doesn't contain winget.
Assert-WinGetPackageManager -Version foo also verifies the installed version is the same as the expected.
Assert-WinGetPackageManager -Latest also verifies the installed version is the latest release version.
Assert-WinGetPackageManager -Latest -IncludePrerelease also verifies the installed version is the latest prerelease version.

Repair-WinGetPackageManager does the same as Assert-WinGetIntegrity but attempts to fix winget depending on the failure. Returns 0 is succeeded.

Repair-WinGetPackageManager repairs current winget installed and performs no updates. If winget is not installed or there's catastrophic failure then installs the latest released winget.
Repair-WinGetPackageManager -Version foo repairs winget and makes sure that the current installed version is the expected one.
Repair-WinGetPackageManager -Latest repairs winget and makes sure winget is the latest released version.
Repair-WinGetPackageManager -Latest -IncludePreRelease repairs winget and makes sure winget is the latest prereleased version.

It sadly can't repair if AppExecution alias is disabled because there's no programmatic way to do it (requires internal APIs).

It also doesn't install Microsoft.UI.Xaml.2.7 as a dependency because the current solution to download the nuget package, extract it and install the appx within is not the right approach we want to follow. We will follow up internally with the owners of the package to do something similar as the VCLibs packages.

There's still a lot of opportunity for Repair-WinGetPackageManager to make it more robust, but this PR is just the starting phase.

The WinGetPackageManager DSC resource is just a simple wrapper around those two cmdlets. There's also a sample on how to use it.

For now, all versions need to be the tag name of the releases in GitHub.

Also, moved Get-WinGetVersion from Crescendo to a binary cmdlet.

---------

Co-authored-by: JohnMcPMS <[email protected]>
  • Loading branch information
msftrubengu and JohnMcPMS authored Mar 21, 2023
1 parent 1898da0 commit fc215c4
Show file tree
Hide file tree
Showing 30 changed files with 2,106 additions and 703 deletions.
3 changes: 3 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ PCCERT
PCs
pcwsz
PEGI
PFM
pfn
pfxpath
Pherson
Expand All @@ -282,6 +283,7 @@ processthreads
productcode
pscustomobject
pseudocode
PSHOST
psobject
ptstr
publickey
Expand All @@ -300,6 +302,7 @@ REFIID
regexes
REGSAM
relativefilepath
remoting
reparse
restsource
rgex
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// -----------------------------------------------------------------------------
// <copyright file="AssertWinGetPackageManagerCommand.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace Microsoft.WinGet.Client.Commands
{
using System.Management.Automation;
using Microsoft.WinGet.Client.Commands.Common;
using Microsoft.WinGet.Client.Common;
using Microsoft.WinGet.Client.Helpers;

/// <summary>
/// Assert-WinGetPackageManager. Verifies winget is installed properly.
/// </summary>
[Cmdlet(
VerbsLifecycle.Assert,
Constants.WinGetNouns.WinGetPackageManager,
DefaultParameterSetName = Constants.IntegrityVersionSet)]
public class AssertWinGetPackageManagerCommand : BaseIntegrityCommand
{
/// <summary>
/// Validates winget is installed correctly. If not, throws an exception
/// with the reason why, if any.
/// </summary>
protected override void ProcessRecord()
{
string expectedVersion = string.Empty;
if (this.ParameterSetName == Constants.IntegrityLatestSet)
{
var gitHubRelease = new GitHubRelease();
expectedVersion = gitHubRelease.GetLatestVersionTagName(this.IncludePreRelease.ToBool());
}
else
{
expectedVersion = this.Version;
}

WinGetIntegrity.AssertWinGet(this, expectedVersion);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// -----------------------------------------------------------------------------
// <copyright file="BaseIntegrityCommand.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace Microsoft.WinGet.Client.Commands.Common
{
using System.Management.Automation;
using Microsoft.WinGet.Client.Common;

/// <summary>
/// Common parameters for Assert-WinGetPackageManager and Repair-WinGetPackageManager.
/// </summary>
public abstract class BaseIntegrityCommand : BaseCommand
{
/// <summary>
/// Gets or sets the optional version.
/// </summary>
[Parameter(
ParameterSetName = Constants.IntegrityVersionSet,
ValueFromPipelineByPropertyName = true)]
public string Version { get; set; } = string.Empty;

/// <summary>
/// Gets or sets a value indicating whether to use latest.
/// </summary>
[Parameter(
ParameterSetName = Constants.IntegrityLatestSet,
ValueFromPipelineByPropertyName = true)]
public SwitchParameter Latest { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to include prerelease winget versions.
/// </summary>
[Parameter(
ParameterSetName = Constants.IntegrityLatestSet,
ValueFromPipelineByPropertyName = true)]
public SwitchParameter IncludePreRelease { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// -----------------------------------------------------------------------------
// <copyright file="GetVersionCommand.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace Microsoft.WinGet.Client.Commands
{
using System.Management.Automation;
using Microsoft.WinGet.Client.Commands.Common;
using Microsoft.WinGet.Client.Common;
using Microsoft.WinGet.Client.Helpers;

/// <summary>
/// Get-WinGetVersion. Gets the current version of winget.
/// </summary>
[Cmdlet(VerbsCommon.Get, Constants.WinGetNouns.Version)]
[OutputType(typeof(string))]
public class GetVersionCommand : BaseCommand
{
/// <summary>
/// Writes the winget version.
/// </summary>
protected override void ProcessRecord()
{
this.WriteObject(WinGetVersion.InstalledWinGetVersion.TagVersion);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// -----------------------------------------------------------------------------
// <copyright file="RepairWinGetPackageManagerCommand.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace Microsoft.WinGet.Client.Commands
{
using System;
using System.Management.Automation;
using Microsoft.WinGet.Client.Commands.Common;
using Microsoft.WinGet.Client.Common;
using Microsoft.WinGet.Client.Helpers;
using Microsoft.WinGet.Client.Properties;

/// <summary>
/// Repair-WinGetPackageManager. Repairs winget if needed.
/// </summary>
[Cmdlet(
VerbsDiagnostic.Repair,
Constants.WinGetNouns.WinGetPackageManager,
DefaultParameterSetName = Constants.IntegrityVersionSet)]
[OutputType(typeof(int))]
public class RepairWinGetPackageManagerCommand : BaseIntegrityCommand
{
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>
/// Attempts to repair winget.
/// TODO: consider WhatIf and Confirm options.
/// </summary>
protected override void ProcessRecord()
{
int result = Failed;

string expectedVersion = this.Version;
if (this.ParameterSetName == Constants.IntegrityLatestSet)
{
var gitHubRelease = new GitHubRelease();
expectedVersion = gitHubRelease.GetLatestVersionTagName(this.IncludePreRelease.ToBool());
}

var integrityCategory = WinGetIntegrity.GetIntegrityCategory(this, expectedVersion);
this.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, expectedVersion);
this.WriteDebug($"Integrity category after fixing PATH {newIntegrityCategory}");
result = this.VerifyWinGetInstall(newIntegrityCategory, expectedVersion);
}
else if (integrityCategory == IntegrityCategory.AppInstallerNotRegistered)
{
var appxModule = new AppxModuleHelper(this);
appxModule.RegisterAppInstaller();

// Now try again and get the desired winget version if needed.
var newIntegrityCategory = WinGetIntegrity.GetIntegrityCategory(this, expectedVersion);
this.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))
{
var gitHubRelease = new GitHubRelease();
expectedVersion = gitHubRelease.GetLatestVersionTagName(false);
}

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

this.WriteObject(result);
}

private int VerifyWinGetInstall(IntegrityCategory integrityCategory, string expectedVersion)
{
if (integrityCategory == IntegrityCategory.Installed)
{
// Nothing to do
this.WriteDebug($"WinGet is in a good state.");
return Succeeded;
}
else if (integrityCategory == IntegrityCategory.UnexpectedVersion)
{
// The versions are different, download and install.
if (!this.InstallDifferentVersion(new WinGetVersion(expectedVersion)))
{
this.WriteDebug($"Failed installing {expectedVersion}");
}
else
{
return Succeeded;
}
}

return Failed;
}

private bool InstallDifferentVersion(WinGetVersion toInstallVersion)
{
var installedVersion = WinGetVersion.InstalledWinGetVersion;

this.WriteDebug($"Installed WinGet version {installedVersion.TagVersion}");
this.WriteDebug($"Installing WinGet version {toInstallVersion.TagVersion}");

bool downgrade = false;
if (installedVersion.CompareAsDeployment(toInstallVersion) > 0)
{
downgrade = true;
}

return this.DownloadAndInstall(toInstallVersion.TagVersion, downgrade);
}

private bool DownloadAndInstall(string versionTag, bool downgrade)
{
// Download and install.
var gitHubRelease = new GitHubRelease();
var downloadedMsixBundlePath = gitHubRelease.DownloadRelease(versionTag);

var appxModule = new AppxModuleHelper(this);
appxModule.AddAppInstallerBundle(downloadedMsixBundlePath, downgrade);

// Verify that is installed
var integrityCategory = WinGetIntegrity.GetIntegrityCategory(this, versionTag);
if (integrityCategory != IntegrityCategory.Installed)
{
return false;
}

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

private void RepairEnvPath()
{
// Add windows app path to user PATH environment variable
Utilities.AddWindowsAppToPath();

// Update this sessions PowerShell environment so the user doesn't have to restart the terminal.
string envPathUser = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.User);
string envPathMachine = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.Machine);
string newPwshPathEnv = $"{envPathMachine};{envPathUser}";
this.SessionState.PSVariable.Set(EnvPath, newPwshPathEnv);

this.WriteDebug($"PATH environment variable updated");
}
}
}
47 changes: 45 additions & 2 deletions src/PowerShell/Microsoft.WinGet.Client/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,51 @@ internal static class Constants
/// This parameter set indicates that a package was not provided via a parameter or the pipeline and it
/// needs to be found by searching a package source.
/// </summary>
public const string FoundSet = "FoundSet";

public const string FoundSet = "FoundSet";

/// <summary>
/// Parameter set for an specific version parameter.
/// </summary>
public const string IntegrityVersionSet = "IntegrityVersionSet";

/// <summary>
/// Parameter set for an latest version with optional prerelease version.
/// </summary>
public const string IntegrityLatestSet = "IntegrityLatestSet";

/// <summary>
/// WinGet package family name.
/// </summary>
#if USE_PROD_CLSIDS
public const string WingetPackageFamilyName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe";
#else
public const string WingetPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe";
#endif

/// <summary>
/// Winget executable name.
/// </summary>
#if USE_PROD_CLSIDS
public const string WinGetExe = "winget.exe";
#else
public const string WinGetExe = "wingetdev.exe";
#endif

/// <summary>
/// Name of PATH environment variable.
/// </summary>
public const string PathEnvVar = "PATH";

/// <summary>
/// Nouns used for different cmdlets. Changing this will alter the names of the related commands.
/// </summary>
public static class WinGetNouns
{
/// <summary>
/// WinGet.
/// </summary>
public const string WinGetPackageManager = "WinGetPackageManager";

/// <summary>
/// The noun analogue of the <see cref="CatalogPackage" /> class.
/// </summary>
Expand All @@ -53,6 +91,11 @@ public static class WinGetNouns
/// The noun for any user settings cmdlet.
/// </summary>
public const string UserSettings = "WinGetUserSettings";

/// <summary>
/// The noun for winget version.
/// </summary>
public const string Version = "WinGetVersion";
}
}
}
Loading

0 comments on commit fc215c4

Please sign in to comment.