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