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-WinGetPackage download VCLibs if needed #3180

Merged
merged 2 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -176,17 +176,20 @@ private bool InstallDifferentVersion(WinGetVersion toInstallVersion)

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.FullFileName);

var appxModule = new AppxModuleHelper(this.PsCmdlet);
appxModule.AddAppInstallerBundle(downloadedMsixBundlePath, downgrade);
appxModule.AddAppInstallerBundle(tempFile.FullFileName, 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,19 @@ public static void AssertWinGet(PSCmdlet psCmdlet, string expectedVersion)
var result = wingetCliWrapper.RunCommand("--version");
result.VerifyExitCode();
}
catch (Win32Exception)
catch (Win32Exception e)
{
psCmdlet.WriteDebug($"'winget.exe --version' Win32Exception {e}");
msftrubengu marked this conversation as resolved.
Show resolved Hide resolved
throw new WinGetIntegrityException(GetReason(psCmdlet));
}
catch (Exception e) when (e is WinGetCLIException || e is WinGetCLITimeoutException)
{
psCmdlet.WriteDebug($"'winget.exe --version' WinGetCLIException {e}");
throw new WinGetIntegrityException(IntegrityCategory.Failure, e);
}
catch (Exception e)
{
psCmdlet.WriteDebug($"'winget.exe --version' Exception {e}");
throw new WinGetIntegrityException(IntegrityCategory.Unknown, e);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

namespace Microsoft.WinGet.Client.Engine.Helpers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
Expand All @@ -25,11 +27,13 @@ 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 AddAppxPackageFormat = "Add-AppxPackage -Path {0} -ErrorAction Stop";
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}";

private const string TryCatchScript = "try {{ {0} }} catch {{ return $_ }}";

private const string AppInstallerName = "Microsoft.DesktopAppInstaller";
private const string AppxManifest = "AppxManifest.xml";
private const string PackageFullName = "PackageFullName";
Expand Down Expand Up @@ -121,13 +125,12 @@ public void AddAppInstallerBundle(string localPath, bool downgrade = false)
sb.Append(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);
var errorRecord = this.RunScript(sb.ToString());
if (errorRecord != null)
{
this.psCmdlet.WriteError(errorRecord);
throw errorRecord.Exception;
}
}

/// <summary>
Expand Down Expand Up @@ -155,11 +158,14 @@ private PSObject GetAppxObject(string packageName)
private IReadOnlyList<string> GetVCLibsDependencies()
{
var vcLibsDependencies = new List<string>();
var vcLibsPackageObjs = this.psCmdlet.InvokeCommand
.InvokeScript(string.Format(GetAppxPackageByVersionCommand, VCLibsUWPDesktop, VCLibsUWPDesktopVersion));

var cmd = string.Format(GetAppxPackageByVersionCommand, VCLibsUWPDesktop, VCLibsUWPDesktopVersion);
this.psCmdlet.WriteDebug($"Running command: '{cmd}'");
var vcLibsPackageObjs = this.psCmdlet.InvokeCommand.InvokeScript(cmd);
msftrubengu marked this conversation as resolved.
Show resolved Hide resolved
if (vcLibsPackageObjs is null ||
vcLibsPackageObjs.Count == 0)
{
this.psCmdlet.WriteDebug("Couldn't find required VCLibs package");
var arch = RuntimeInformation.OSArchitecture;
if (arch == Architecture.X64)
{
Expand Down Expand Up @@ -195,8 +201,7 @@ private void InstallVCLibsDependencies()
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);
}
}

Expand All @@ -210,5 +215,64 @@ private void InstallUiXaml()
throw new PSNotImplementedException(Resources.MicrosoftUIXaml27Message);
}
}

private void AddAppxPackageAsUri(string packageUri)
{
var errorRecord = this.RunScript(string.Format(AddAppxPackageFormat, packageUri));

if (errorRecord != null)
{
if (errorRecord.CategoryInfo.Category == ErrorCategory.OpenError)
{
this.psCmdlet.WriteDebug($"Failed adding package {packageUri}. Retrying downloading it.");
msftrubengu marked this conversation as resolved.
Show resolved Hide resolved
this.DownloadPackageAndAdd(packageUri);
}
else
{
this.psCmdlet.WriteError(errorRecord);
throw errorRecord.Exception;
}
}
}

private void DownloadPackageAndAdd(string packageUrl)
{
var tempFile = new TempFile();

// This is weird but easy.
var githubRelease = new GitHubRelease();
githubRelease.DownloadUrl(packageUrl, tempFile.FullFileName);

var errorRecord = this.RunScript(string.Format(AddAppxPackageFormat, tempFile.FullFileName));

if (errorRecord is not null)
{
this.psCmdlet.WriteError(errorRecord);
throw errorRecord.Exception;
}
}

private ErrorRecord RunScript(string innerScript)
{
var script = string.Format(
TryCatchScript,
innerScript);

this.psCmdlet.WriteDebug($"Running script {script}");
var result = this.psCmdlet.InvokeCommand.InvokeScript(
script,
useNewScope: true,
PipelineResultTypes.Error,
input: null);

if (result is not null &&
result.Count > 0)
{
var errorRecord = (ErrorRecord)result[0].ImmediateBaseObject;
return errorRecord;
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ public GitHubRelease()
/// 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>
Expand All @@ -69,17 +69,16 @@ public void DownloadUrl(string url, string fileName)
/// 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>
Expand All @@ -96,7 +95,7 @@ public async Task DownloadUrlAsync(string url, string fileName)
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);
}
Expand Down
110 changes: 110 additions & 0 deletions src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs
Original file line number Diff line number Diff line change
@@ -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.FullFileName = Path.Combine(Path.GetTempPath(), this.FileName);

if (deleteIfExists && File.Exists(this.FullFileName))
{
File.Delete(this.FullFileName);
}

if (!string.IsNullOrWhiteSpace(content))
{
this.CreateFile(content);
}

this.cleanup = cleanup;
}

/// <summary>
/// Gets the file name.
/// </summary>
public string FileName { get; }

/// <summary>
/// Gets the full file name.
/// </summary>
public string FullFileName { get; }
msftrubengu marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// IDisposable.Dispose .
msftrubengu marked this conversation as resolved.
Show resolved Hide resolved
/// </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.FullFileName);
}
else
{
File.WriteAllText(this.FullFileName, 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.FullFileName))
{
File.Delete(this.FullFileName);
}

this.disposed = true;
}
}
}
}