diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index 45d5938395..722ab72ce6 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -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.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 index 27638b2fd0..ff9e99aabe 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -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' 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 index 5c2e81c220..582e955fb9 100644 --- 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 /// 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 @@ internal class AppxModuleHelper 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 } /// @@ -113,21 +112,28 @@ public void AddAppInstallerBundle(string localPath, bool downgrade = false) this.InstallVCLibsDependencies(); this.InstallUiXaml(); - StringBuilder sb = new StringBuilder(); - sb.Append(string.Format(AddAppxPackageFormat, localPath)); - + var options = new List(); 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 + { + { Path, localPath }, + { ErrorAction, Stop }, + }, + options); + } + catch (RuntimeException e) + { + this.psCmdlet.WriteError(e.ErrorRecord); + throw e; + } } /// @@ -136,30 +142,64 @@ public void AddAppInstallerBundle(string localPath, bool downgrade = false) 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 + { + { Path, appxManifestPath }, + }, + new List + { + Register, + DisableDevelopmentMode, + }); } private PSObject GetAppxObject(string packageName) { - return this.psCmdlet.InvokeCommand - .InvokeScript(string.Format(GetAppxPackageCommand, packageName)) + return this.ExecuteAppxCmdlet( + GetAppxPackage, + new Dictionary + { + { Name, packageName }, + }) .FirstOrDefault(); } private IReadOnlyList GetVCLibsDependencies() { var vcLibsDependencies = new List(); - 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 + { + { 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 @@ 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); } } @@ -210,5 +249,95 @@ private void InstallUiXaml() throw new PSNotImplementedException(Resources.MicrosoftUIXaml27Message); } } + + private void AddAppxPackageAsUri(string packageUri) + { + try + { + _ = this.ExecuteAppxCmdlet( + AddAppxPackage, + new Dictionary + { + { 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 + { + { Path, tempFile.FullPath }, + { ErrorAction, Stop }, + }); + } + + private Collection ExecuteAppxCmdlet(string cmdlet, Dictionary parameters = null, IList 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 index ff7bee74f8..b8da873beb 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs @@ -39,10 +39,10 @@ public GitHubRelease() /// Download a release from winget-cli. /// /// Optional release name. If null, gets latest. - /// Path where the msix bundle is downloaded. - public string DownloadRelease(string releaseTag) + /// Output file. + public void DownloadRelease(string releaseTag, string outputFile) { - return this.DownloadReleaseAsync(releaseTag).GetAwaiter().GetResult(); + this.DownloadReleaseAsync(releaseTag, outputFile).GetAwaiter().GetResult(); } /// @@ -69,17 +69,16 @@ public void DownloadUrl(string url, string fileName) /// Download asynchronously a release from winget-cli. /// /// Optional release name. If null, gets latest. - /// Path where the msix bundle is downloaded. - public async Task DownloadReleaseAsync(string releaseTag) + /// Output file. + /// A representing the asynchronous operation. + 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); } /// @@ -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); } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs new file mode 100644 index 0000000000..6034dcb864 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs @@ -0,0 +1,110 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.IO; + + /// + /// Creates a temporary file in the user's temporary directory. + /// + internal class TempFile : IDisposable + { + private readonly bool cleanup; + + private bool disposed = false; + + /// + /// Initializes a new instance of the class. + /// + /// Optional file name. If null, creates a random file name. + /// Delete file if already exists. Default true. + /// Optional content. If not null or empty, creates file and writes to it. + /// Deletes file at disposing time. Default true. + 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; + } + + /// + /// Gets the file name. + /// + public string FileName { get; } + + /// + /// Gets the full path. + /// + public string FullPath { get; } + + /// + /// IDisposable.Dispose. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Creates the file. + /// + /// Content. + public void CreateFile(string content = null) + { + if (content is null) + { + using var fs = File.Create(this.FullPath); + } + else + { + File.WriteAllText(this.FullPath, content); + } + } + + /// + /// Protected disposed. + /// + /// Disposing. + 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 index e191dee681..0867529b27 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj @@ -35,6 +35,7 @@ +