winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 26a2173bc1a9ac0b3c5c66802a4fb44f16361fe1
parent f74a4e0dfe31cbd5d2cacf9a9cba91d0bfc22c48
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Fri, 28 Apr 2023 11:18:54 -0700

Repair-WinGetPackage download VCLibs if needed (#3180)

Add-AppxPackage -Path <vclibs urls> fails in Windows Sandbox. This change detects it and download the package and installs is from a file path.

Add more debug information to Repair and Assert cmdlets.
Diffstat:
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs | 7+++++--
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs | 5++++-
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs | 219++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs | 17++++++++---------
Asrc/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj | 1+
6 files changed, 302 insertions(+), 57 deletions(-)

diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -176,17 +176,20 @@ namespace Microsoft.WinGet.Client.Engine.Commands private bool DownloadAndInstall(string versionTag, bool downgrade) { + using var tempFile = new TempFile(); + // Download and install. var gitHubRelease = new GitHubRelease(); - var downloadedMsixBundlePath = gitHubRelease.DownloadRelease(versionTag); + gitHubRelease.DownloadRelease(versionTag, tempFile.FullPath); var appxModule = new AppxModuleHelper(this.PsCmdlet); - appxModule.AddAppInstallerBundle(downloadedMsixBundlePath, downgrade); + 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; } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -46,16 +46,19 @@ namespace Microsoft.WinGet.Client.Engine.Common var result = wingetCliWrapper.RunCommand("--version"); result.VerifyExitCode(); } - catch (Win32Exception) + catch (Win32Exception e) { + psCmdlet.WriteDebug($"'winget.exe' Win32Exception {e.Message}"); throw new WinGetIntegrityException(GetReason(psCmdlet)); } catch (Exception e) when (e is WinGetCLIException || e is WinGetCLITimeoutException) { + psCmdlet.WriteDebug($"'winget.exe' WinGetCLIException {e.Message}"); throw new WinGetIntegrityException(IntegrityCategory.Failure, e); } catch (Exception e) { + psCmdlet.WriteDebug($"'winget.exe' Exception {e.Message}"); throw new WinGetIntegrityException(IntegrityCategory.Unknown, e); } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs @@ -7,13 +7,10 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { using System.Collections.Generic; - using System.IO; + using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; - using System.Management.Automation.Runspaces; - using System.Resources; using System.Runtime.InteropServices; - using System.Text; using Microsoft.WinGet.Client.Engine.Common; using Microsoft.WinGet.Client.Engine.Properties; @@ -22,13 +19,27 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// </summary> internal class AppxModuleHelper { - private const string GetAppxModule = "Get-Module Appx"; - private const string ImportModuleCore = "Import-Module Appx -UseWindowsPowerShell"; - private const string GetAppxPackageCommand = "Get-AppxPackage {0}"; - private const string AddAppxPackageFormat = "Add-AppxPackage -Path {0}"; - private const string AddAppxPackageRegisterFormat = "Add-AppxPackage -Path {0} -Register -DisableDevelopmentMode"; - private const string ForceUpdateFromAnyVersion = " -ForceUpdateFromAnyVersion"; - private const string GetAppxPackageByVersionCommand = "Get-AppxPackage {0} | Where-Object -Property Version -eq {1}"; + // Cmdlets + private const string ImportModule = "Import-Module"; + private const string GetAppxPackage = "Get-AppxPackage"; + private const string AddAppxPackage = "Add-AppxPackage"; + + // Parameters name + private const string Name = "Name"; + private const string Path = "Path"; + private const string ErrorAction = "ErrorAction"; + private const string WarningAction = "WarningAction"; + + // Parameter Values + private const string Appx = "Appx"; + private const string Stop = "Stop"; + private const string SilentlyContinue = "SilentlyContinue"; + + // Options + private const string UseWindowsPowerShell = "UseWindowsPowerShell"; + private const string ForceUpdateFromAnyVersion = "ForceUpdateFromAnyVersion"; + private const string Register = "Register"; + private const string DisableDevelopmentMode = "DisableDevelopmentMode"; private const string AppInstallerName = "Microsoft.DesktopAppInstaller"; private const string AppxManifest = "AppxManifest.xml"; @@ -53,18 +64,6 @@ namespace Microsoft.WinGet.Client.Engine.Helpers public AppxModuleHelper(PSCmdlet psCmdlet) { this.psCmdlet = psCmdlet; - - // There's a bug in the Appx Module that it can't be loaded from Core in pre 10.0.22453.0 builds without - // the -UseWindowsPowerShell option. In post 10.0.22453.0 builds there's really no difference between - // using or not -UseWindowsPowerShell as it will automatically get loaded using WinPSCompatSession remoting session. - // https://github.com/PowerShell/PowerShell/issues/13138. -#if !POWERSHELL_WINDOWS - var appxModule = this.psCmdlet.InvokeCommand.InvokeScript(GetAppxModule); - if (appxModule is null) - { - this.psCmdlet.InvokeCommand.InvokeScript(ImportModuleCore); - } -#endif } /// <summary> @@ -113,21 +112,28 @@ namespace Microsoft.WinGet.Client.Engine.Helpers this.InstallVCLibsDependencies(); this.InstallUiXaml(); - StringBuilder sb = new StringBuilder(); - sb.Append(string.Format(AddAppxPackageFormat, localPath)); - + var options = new List<string>(); if (downgrade) { - sb.Append(ForceUpdateFromAnyVersion); + options.Add(ForceUpdateFromAnyVersion); } - // Using this method simplifies a lot of things, but the error is not propagated with - // the default parameters. PipelineResultTypes.Error will at least output it in the terminal. - this.psCmdlet.InvokeCommand.InvokeScript( - sb.ToString(), - useNewScope: true, - PipelineResultTypes.Error, - input: null); + try + { + _ = this.ExecuteAppxCmdlet( + AddAppxPackage, + new Dictionary<string, object> + { + { Path, localPath }, + { ErrorAction, Stop }, + }, + options); + } + catch (RuntimeException e) + { + this.psCmdlet.WriteError(e.ErrorRecord); + throw e; + } } /// <summary> @@ -136,30 +142,64 @@ namespace Microsoft.WinGet.Client.Engine.Helpers public void RegisterAppInstaller() { string packageFullName = this.GetAppInstallerPropertyValue(PackageFullName); - string appxManifestPath = Path.Combine( + string appxManifestPath = System.IO.Path.Combine( Utilities.ProgramFilesWindowsAppPath, packageFullName, AppxManifest); - this.psCmdlet.InvokeCommand.InvokeScript( - string.Format(AddAppxPackageRegisterFormat, appxManifestPath)); + _ = this.ExecuteAppxCmdlet( + AddAppxPackage, + new Dictionary<string, object> + { + { Path, appxManifestPath }, + }, + new List<string> + { + Register, + DisableDevelopmentMode, + }); } private PSObject GetAppxObject(string packageName) { - return this.psCmdlet.InvokeCommand - .InvokeScript(string.Format(GetAppxPackageCommand, packageName)) + return this.ExecuteAppxCmdlet( + GetAppxPackage, + new Dictionary<string, object> + { + { Name, packageName }, + }) .FirstOrDefault(); } private IReadOnlyList<string> GetVCLibsDependencies() { var vcLibsDependencies = new List<string>(); - var vcLibsPackageObjs = this.psCmdlet.InvokeCommand - .InvokeScript(string.Format(GetAppxPackageByVersionCommand, VCLibsUWPDesktop, VCLibsUWPDesktopVersion)); - if (vcLibsPackageObjs is null || - vcLibsPackageObjs.Count == 0) + + var result = this.ExecuteAppxCmdlet( + GetAppxPackage, + new Dictionary<string, object> + { + { Name, VCLibsUWPDesktop }, + }); + + // See if the required version is installed. + bool isInstalled = false; + if (result != null && + result.Count > 0) + { + foreach (dynamic psobject in result) + { + if (psobject?.Version == VCLibsUWPDesktopVersion) + { + isInstalled = true; + break; + } + } + } + + if (!isInstalled) { + this.psCmdlet.WriteDebug("Couldn't find required VCLibs package"); var arch = RuntimeInformation.OSArchitecture; if (arch == Architecture.X64) { @@ -195,8 +235,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers var packages = this.GetVCLibsDependencies(); foreach (var package in packages) { - this.psCmdlet.WriteDebug($"Installing VCLibs {package}"); - this.psCmdlet.InvokeCommand.InvokeScript(string.Format(AddAppxPackageFormat, package)); + this.AddAppxPackageAsUri(package); } } @@ -210,5 +249,95 @@ namespace Microsoft.WinGet.Client.Engine.Helpers throw new PSNotImplementedException(Resources.MicrosoftUIXaml27Message); } } + + private void AddAppxPackageAsUri(string packageUri) + { + try + { + _ = this.ExecuteAppxCmdlet( + AddAppxPackage, + new Dictionary<string, object> + { + { Path, packageUri }, + { ErrorAction, Stop }, + }); + } + catch (RuntimeException e) + { + // If we couldn't install it via URI, try download and install. + if (e.ErrorRecord.CategoryInfo.Category == ErrorCategory.OpenError) + { + this.psCmdlet.WriteDebug($"Failed adding package [{packageUri}]. Retrying downloading it."); + this.DownloadPackageAndAdd(packageUri); + } + else + { + this.psCmdlet.WriteError(e.ErrorRecord); + throw e; + } + } + } + + private void DownloadPackageAndAdd(string packageUrl) + { + var tempFile = new TempFile(); + + // This is weird but easy. + var githubRelease = new GitHubRelease(); + githubRelease.DownloadUrl(packageUrl, tempFile.FullPath); + + _ = this.ExecuteAppxCmdlet( + AddAppxPackage, + new Dictionary<string, object> + { + { Path, tempFile.FullPath }, + { ErrorAction, Stop }, + }); + } + + private Collection<PSObject> ExecuteAppxCmdlet(string cmdlet, Dictionary<string, object> parameters = null, IList<string> options = null) + { + var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); + + // There's a bug in the Appx Module that it can't be loaded from Core in pre 10.0.22453.0 builds without + // the -UseWindowsPowerShell option. In post 10.0.22453.0 builds there's really no difference between + // using or not -UseWindowsPowerShell as it will automatically get loaded using WinPSCompatSession remoting session. + // https://github.com/PowerShell/PowerShell/issues/13138. + // Set warning action to silently continue to avoid the console with + // 'Module Appx is loaded in Windows PowerShell using WinPSCompatSession remoting session' +#if !POWERSHELL_WINDOWS + ps.AddCommand(ImportModule) + .AddParameter(Name, Appx) + .AddParameter(UseWindowsPowerShell) + .AddParameter(WarningAction, SilentlyContinue) + .AddStatement(); +#endif + + string cmd = cmdlet; + ps.AddCommand(cmdlet); + + if (parameters != null) + { + foreach (var p in parameters) + { + cmd += $" -{p.Key} {p.Value}"; + } + + ps.AddParameters(parameters); + } + + if (options != null) + { + foreach (var option in options) + { + cmd += $" -{option}"; + ps.AddParameter(option); + } + } + + this.psCmdlet.WriteDebug($"Executing Appx cmdlet {cmd}"); + var result = ps.Invoke(); + return result; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs @@ -39,10 +39,10 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// Download a release from winget-cli. /// </summary> /// <param name="releaseTag">Optional release name. If null, gets latest.</param> - /// <returns>Path where the msix bundle is downloaded.</returns> - public string DownloadRelease(string releaseTag) + /// <param name="outputFile">Output file.</param> + public void DownloadRelease(string releaseTag, string outputFile) { - return this.DownloadReleaseAsync(releaseTag).GetAwaiter().GetResult(); + this.DownloadReleaseAsync(releaseTag, outputFile).GetAwaiter().GetResult(); } /// <summary> @@ -69,17 +69,16 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// Download asynchronously a release from winget-cli. /// </summary> /// <param name="releaseTag">Optional release name. If null, gets latest.</param> - /// <returns>Path where the msix bundle is downloaded.</returns> - public async Task<string> DownloadReleaseAsync(string releaseTag) + /// <param name="outputFile">Output file.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + public async Task DownloadReleaseAsync(string releaseTag, string outputFile) { Release release = await this.gitHubClient.Repository.Release.Get(Owner, Repo, releaseTag); // Get asset and download. var msixBundleAsset = release.Assets.Where(a => a.Name == MsixBundleName).First(); - var tmpFile = Path.GetTempFileName(); - await this.DownloadUrlAsync(msixBundleAsset.Url, tmpFile); - return tmpFile; + await this.DownloadUrlAsync(msixBundleAsset.Url, outputFile); } /// <summary> @@ -96,7 +95,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers ContentType); using var memoryStream = new MemoryStream((byte[])response.Body); - using var fileStream = File.Open(fileName, FileMode.Open); + using var fileStream = File.Open(fileName, FileMode.OpenOrCreate); memoryStream.Position = 0; await memoryStream.CopyToAsync(fileStream); } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs @@ -0,0 +1,110 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TempFile.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.IO; + + /// <summary> + /// Creates a temporary file in the user's temporary directory. + /// </summary> + internal class TempFile : IDisposable + { + private readonly bool cleanup; + + private bool disposed = false; + + /// <summary> + /// Initializes a new instance of the <see cref="TempFile"/> class. + /// </summary> + /// <param name="fileName">Optional file name. If null, creates a random file name.</param> + /// <param name="deleteIfExists">Delete file if already exists. Default true.</param> + /// <param name="content">Optional content. If not null or empty, creates file and writes to it.</param> + /// <param name="cleanup">Deletes file at disposing time. Default true.</param> + public TempFile( + string fileName = null, + bool deleteIfExists = true, + string content = null, + bool cleanup = true) + { + if (fileName is null) + { + this.FileName = Path.GetRandomFileName(); + } + else + { + this.FileName = fileName; + } + + this.FullPath = Path.Combine(Path.GetTempPath(), this.FileName); + + if (deleteIfExists && File.Exists(this.FullPath)) + { + File.Delete(this.FullPath); + } + + if (!string.IsNullOrWhiteSpace(content)) + { + this.CreateFile(content); + } + + this.cleanup = cleanup; + } + + /// <summary> + /// Gets the file name. + /// </summary> + public string FileName { get; } + + /// <summary> + /// Gets the full path. + /// </summary> + public string FullPath { get; } + + /// <summary> + /// IDisposable.Dispose. + /// </summary> + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Creates the file. + /// </summary> + /// <param name="content">Content.</param> + public void CreateFile(string content = null) + { + if (content is null) + { + using var fs = File.Create(this.FullPath); + } + else + { + File.WriteAllText(this.FullPath, content); + } + } + + /// <summary> + /// Protected disposed. + /// </summary> + /// <param name="disposing">Disposing.</param> + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (this.cleanup && File.Exists(this.FullPath)) + { + File.Delete(this.FullPath); + } + + this.disposed = true; + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj b/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj @@ -36,6 +36,7 @@ </PackageReference> <PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> + <PackageReference Include="Microsoft.CSharp" Version="4.7.0" Condition="'$(TargetFramework)' == '$(DesktopFramework)'"/> </ItemGroup> <ItemGroup>