commit 1ff0b2832cd465ba66acab00b8c6f86c2010d4de parent fb540a28c54a4ec9fbd688985570cd2fcc118c1d Author: Ruben Guerrero <rubengu@microsoft.com> Date: Tue, 5 Dec 2023 09:59:33 -0800 Improve repair (#3886) This PR makes some improvements to the `Microsoft.WinGet.Client` module **Use stub preference for AppInstaller** The default behavior for `Add-AppxPackage` is to install the full package regardless of the preference. This PR adds `-StubPreference UsePreference` (if the cmdlet supports it) when installing the AppInstaller bundle. I originally want to allow the user to set the preference, but I realized I would have to blindly call `winget configure --enable|disable` since I don't know the state. I believe this should be done differently with perhaps different cmdlets and add something like `winget config --info` that prints a json with some information like if its enabled or not and the processor info. **Add -Force to Repair-WinGetPackage** On some occasions, `Add-AppxPackage` failed because AppInstaller files were in use. I added a `-Force` switch to the cmdlet that just sets `-ForceTargetApplicationShutdown` on the add appx package call. **Module async friendly** All async functions in the module required to be waited. Now that we can start an async context via `RunOnMTA`, I removed all the synchronous wrappers calls from GitHubClient and make it such as only the top-level cmdlet need to wait on the Task. This caused a lot of functions to be awaited, but at the end only the top-level cmdlet should care about manually waiting for the task which I believe is the right thing to do. **Bring back synchronization with main PowerShell thread** AppxModuleHelper requires its Appx Module calls to be executed in the main PowerShell thread otherwise a new runtime would need to be created for every cmdlet that uses this class. I brought back the old synchronization mechanism of `Microsoft.WinGet.Configure` to do this. Now any cmdlet that runs asynchronously and requires PowerShell Host can do it without creating its runspace. **Redo progress for Install/Update/Uninstall-WinGetPackage** I was not a huge fan of how progress was handled in these cmdlets. Given that I can now create an asynchronous context, I simplify how progress is done and follow a similar pattern as `Microsoft.WinGet.Configure`. The `IAsyncOperationWithProgress` is executed, set its Progress handler, converted to a Task and awaited all in one common method. I also fixed how the progress was calculated so it reflects almost the same as winget. .NET rounded the number so for some package I was 92.3 MB in winget and 92.4 the module (before it was like 97or something). **Add proper can cancellation support** Modify the Install/Update/Uninstall cmdlets to properly use PowerShell's `StopProcessing` for cancellation. The Task from `IAsyncOperationWithProgress` uses the PowerShellCmdlet CancellationToken, so everything is handled correctly. Also add cancellation to `Repair-WinGetPackage`. The repair state machine sees if the token is cancelled as the first thing on the loop. **Download URL with progress.** The module attempts to install a package using `Add-AppxPackage` with the url of the package. In the case it fails, like in Windows Sandbox, the package is downloaded to disk and then installed via the same cmdlet. Depending on the network, the download could take a while and there's no indication that work is being performed without `-Verbose`. This PR adds a PowerShell progress bar for the download when the response contains a Content-Length property. If cancellation is request during url download, the async calls will be cancelled the file will be deleted. Diffstat:
27 files changed, 901 insertions(+), 485 deletions(-)
diff --git a/src/PowerShell/CommonFiles/PowerShellCmdlet.cs b/src/PowerShell/CommonFiles/PowerShellCmdlet.cs @@ -10,6 +10,7 @@ namespace Microsoft.WinGet.Common.Command using System.Collections.Concurrent; using System.Collections.Generic; using System.Management.Automation; + using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Microsoft.WinGet.Resources; @@ -30,13 +31,18 @@ namespace Microsoft.WinGet.Common.Command private static readonly string[] WriteInformationTags = new string[] { "PSHOST" }; private readonly PSCmdlet psCmdlet; - private readonly Thread originalThread; + private readonly Thread pwshThread; private readonly CancellationTokenSource source = new (); - private BlockingCollection<QueuedStream> queuedStreams = new (); + private readonly SemaphoreSlim semaphore = new (1, 1); + private readonly ManualResetEventSlim pwshThreadActionReady = new (false); + private readonly ManualResetEventSlim pwshThreadActionCompleted = new (false); + private BlockingCollection<QueuedStream> queuedStreams = new (); private int progressActivityId = 0; private ConcurrentDictionary<int, ProgressRecordType> progressRecords = new (); + private Action? pwshThreadAction = null; + private ExceptionDispatchInfo? pwshThreadEdi = null; /// <summary> /// Initializes a new instance of the <see cref="PowerShellCmdlet"/> class. @@ -57,7 +63,7 @@ namespace Microsoft.WinGet.Common.Command this.ValidatePolicies(policies); this.psCmdlet = psCmdlet; - this.originalThread = Thread.CurrentThread; + this.pwshThread = Thread.CurrentThread; } /// <summary> @@ -81,7 +87,7 @@ namespace Microsoft.WinGet.Common.Command throw new NotImplementedException(); #else // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) + if (this.pwshThread != Thread.CurrentThread) { throw new InvalidOperationException(); } @@ -134,7 +140,7 @@ namespace Microsoft.WinGet.Common.Command internal Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func) { // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) + if (this.pwshThread != Thread.CurrentThread) { throw new InvalidOperationException(); } @@ -186,7 +192,7 @@ namespace Microsoft.WinGet.Common.Command internal TResult RunOnMTA<TResult>(Func<TResult> func) { // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) + if (this.pwshThread != Thread.CurrentThread) { throw new InvalidOperationException(); } @@ -230,6 +236,26 @@ namespace Microsoft.WinGet.Common.Command } /// <summary> + /// Executes an action in the main thread. + /// Blocks until call is executed. + /// </summary> + /// <param name="action">Action to perform.</param> + internal void ExecuteInPowerShellThread(Action action) + { + if (this.pwshThread == Thread.CurrentThread) + { + action(); + return; + } + + this.WaitForOurTurn(); + + this.pwshThreadAction = action; + this.pwshThreadActionReady.Set(); + this.WaitMainThreadActionCompletion(); + } + + /// <summary> /// Waits for the task to be completed. This MUST be called from the main thread. /// </summary> /// <param name="runningTask">Task to wait for.</param> @@ -239,14 +265,54 @@ namespace Microsoft.WinGet.Common.Command writeCmdlet ??= this; // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) + if (this.pwshThread != Thread.CurrentThread) { throw new InvalidOperationException(); } do { - this.ConsumeAndWriteStreams(writeCmdlet); + if (this.pwshThreadActionReady.IsSet) + { + // Someone needs the main thread. + this.pwshThreadActionReady.Reset(); + + if (this.pwshThreadAction != null) + { + try + { + this.pwshThreadAction(); + } + catch (Exception e) + { + // Make sure we don't throw in the PowerShell thread, this way + // we'll get a more meaningful stack by Get-Error. + this.pwshThreadEdi = ExceptionDispatchInfo.Capture(e); + } + + this.pwshThreadAction = null; + } + + // Done. + this.pwshThreadActionCompleted.Set(); + } + + // Take from the blocking collection. + if (!this.queuedStreams.IsCompleted && this.queuedStreams.Count > 0) + { + try + { + var queuedOutput = this.queuedStreams.Take(); + if (queuedOutput != null) + { + this.CmdletWrite(queuedOutput.Type, queuedOutput.Data, writeCmdlet); + } + } + catch (InvalidOperationException) + { + // An InvalidOperationException means that Take() was called on a completed collection. + } + } } while (!(runningTask.IsCompleted && this.queuedStreams.IsCompleted)); @@ -254,6 +320,12 @@ namespace Microsoft.WinGet.Common.Command { // If IsFaulted is true, the task's Status is equal to Faulted, // and its Exception property will be non-null. + AggregateException? ae = runningTask.Exception! as AggregateException; + if (ae != null && ae.InnerExceptions.Count == 1) + { + ExceptionDispatchInfo.Capture(ae.InnerExceptions[0]).Throw(); + } + throw runningTask.Exception!; } } @@ -269,15 +341,17 @@ namespace Microsoft.WinGet.Common.Command { if (type == StreamType.Progress) { - // Keep track of all progress activity. ProgressRecord progressRecord = (ProgressRecord)data; - if (!this.progressRecords.TryAdd(progressRecord.ActivityId, progressRecord.RecordType)) + if (progressRecord.RecordType == ProgressRecordType.Completed) { - _ = this.progressRecords.TryUpdate(progressRecord.ActivityId, progressRecord.RecordType, ProgressRecordType.Completed); + throw new NotSupportedException("Use CompleteProgress"); } + + // Keep track of all progress activity. + _ = this.progressRecords.TryAdd(progressRecord.ActivityId, progressRecord.RecordType); } - if (this.originalThread == Thread.CurrentThread) + if (this.pwshThread == Thread.CurrentThread) { this.CmdletWrite(type, data, this); return; @@ -311,26 +385,49 @@ namespace Microsoft.WinGet.Common.Command /// <param name="activityId">Activity id.</param> /// <param name="activity">The activity in progress.</param> /// <param name="status">The status of the activity.</param> - internal void CompleteProgress(int activityId, string activity, string status) + /// <param name="force">Force write complete progress.</param> + internal void CompleteProgress(int activityId, string activity, string status, bool force = false) { var record = new ProgressRecord(activityId, activity, status) { RecordType = ProgressRecordType.Completed, PercentComplete = 100, }; - this.Write(StreamType.Progress, record); + + if (!this.progressRecords.TryAdd(activityId, record.RecordType)) + { + _ = this.progressRecords.TryUpdate(activityId, record.RecordType, ProgressRecordType.Processing); + } + + if (this.pwshThread == Thread.CurrentThread) + { + this.CmdletWrite(StreamType.Progress, record, this); + } + else + { + // You should only use force if you know the cmdlet that is completing this progress is a sync cmdlet that + // is running in an async context. A sync cmdlet is anything that doesn't start with Start-* + if (force) + { + this.ExecuteInPowerShellThread(() => this.CmdletWrite(StreamType.Progress, record, this)); + } + else + { + this.queuedStreams.Add(new QueuedStream(StreamType.Progress, record)); + } + } } /// <summary> /// Writes to PowerShell streams. /// This method must be called in the original thread. - /// WARNING: You must only call this when the task is completed or in Wait. + /// WARNING: You must only call this when the task is completed. /// </summary> /// <param name="writeCmdlet">The cmdlet that can write to PowerShell.</param> internal void ConsumeAndWriteStreams(PowerShellCmdlet writeCmdlet) { // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) + if (this.pwshThread != Thread.CurrentThread) { throw new InvalidOperationException(); } @@ -399,7 +496,7 @@ namespace Microsoft.WinGet.Common.Command internal bool ShouldProcess(string target) { // If not on the main thread just continue. - if (this.originalThread != Thread.CurrentThread) + if (this.pwshThread != Thread.CurrentThread) { return true; } @@ -430,7 +527,7 @@ namespace Microsoft.WinGet.Common.Command case StreamType.Progress: // If the activity is already completed don't write progress. var progressRecord = (ProgressRecord)data; - if (this.progressRecords[progressRecord.ActivityId] == ProgressRecordType.Processing) + if (this.progressRecords[progressRecord.ActivityId] == progressRecord.RecordType) { writeCmdlet.psCmdlet.WriteProgress(progressRecord); } @@ -485,6 +582,28 @@ namespace Microsoft.WinGet.Common.Command } } + private void WaitForOurTurn() + { + this.semaphore.Wait(this.GetCancellationToken()); + this.pwshThreadActionCompleted.Reset(); + } + + private void WaitMainThreadActionCompletion() + { + WaitHandle.WaitAny(new[] + { + this.GetCancellationToken().WaitHandle, + this.pwshThreadActionCompleted.WaitHandle, + }); + + if (this.pwshThreadEdi != null) + { + this.pwshThreadEdi.Throw(); + } + + this.semaphore.Release(); + } + private class QueuedStream { public QueuedStream(StreamType type, object data) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/InstallPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/InstallPackageCmdlet.cs @@ -24,6 +24,8 @@ namespace Microsoft.WinGet.Client.Commands [OutputType(typeof(PSInstallResult))] public sealed class InstallPackageCmdlet : InstallCmdlet { + private InstallerPackageCommand command = null; + /// <summary> /// Gets or sets the scope to install the application under. /// </summary> @@ -41,9 +43,8 @@ namespace Microsoft.WinGet.Client.Commands /// </summary> protected override void ProcessRecord() { - var command = new InstallerPackageCommand( + this.command = new InstallerPackageCommand( this, - this.Mode.ToString(), this.Override, this.Custom, this.Location, @@ -58,7 +59,18 @@ namespace Microsoft.WinGet.Client.Commands this.Moniker, this.Source, this.Query); - command.Install(this.Scope.ToString(), this.Architecture.ToString(), this.MatchOption.ToString(), this.Mode.ToString()); + this.command.Install(this.Scope.ToString(), this.Architecture.ToString(), this.MatchOption.ToString(), this.Mode.ToString()); + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs @@ -21,6 +21,8 @@ namespace Microsoft.WinGet.Client.Commands [OutputType(typeof(int))] public class RepairWinGetPackageManagerCmdlet : WinGetPackageManagerCmdlet { + private WinGetPackageManagerCommand command = null; + /// <summary> /// Gets or sets a value indicating whether to repair for all users. Requires admin. /// </summary> @@ -28,19 +30,36 @@ namespace Microsoft.WinGet.Client.Commands public SwitchParameter AllUsers { get; set; } /// <summary> + /// Gets or sets a value indicating whether to force application shutdown. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// <summary> /// Attempts to repair winget. /// TODO: consider WhatIf and Confirm options. /// </summary> protected override void ProcessRecord() { - var command = new WinGetPackageManagerCommand(this); + this.command = new WinGetPackageManagerCommand(this); if (this.ParameterSetName == Constants.IntegrityLatestSet) { - command.RepairUsingLatest(this.IncludePreRelease.ToBool(), this.AllUsers.ToBool()); + this.command.RepairUsingLatest(this.IncludePreRelease.ToBool(), this.AllUsers.ToBool(), this.Force.ToBool()); } else { - command.Repair(this.Version, this.AllUsers.ToBool()); + this.command.Repair(this.Version, this.AllUsers.ToBool(), this.Force.ToBool()); + } + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UninstallPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UninstallPackageCmdlet.cs @@ -24,6 +24,8 @@ namespace Microsoft.WinGet.Client.Commands [OutputType(typeof(PSUninstallResult))] public sealed class UninstallPackageCmdlet : PackageCmdlet { + private UninstallPackageCommand command = null; + /// <summary> /// Gets or sets the desired mode for the uninstallation process. /// </summary> @@ -41,7 +43,7 @@ namespace Microsoft.WinGet.Client.Commands /// </summary> protected override void ProcessRecord() { - var command = new UninstallPackageCommand( + this.command = new UninstallPackageCommand( this, this.PSCatalogPackage, this.Version, @@ -51,7 +53,18 @@ namespace Microsoft.WinGet.Client.Commands this.Moniker, this.Source, this.Query); - command.Uninstall(this.Mode.ToString(), this.MatchOption.ToString(), this.Force.ToBool()); + this.command.Uninstall(this.Mode.ToString(), this.MatchOption.ToString(), this.Force.ToBool()); + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UpdatePackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UpdatePackageCmdlet.cs @@ -36,7 +36,6 @@ namespace Microsoft.WinGet.Client.Commands { var command = new InstallerPackageCommand( this, - this.Mode.ToString(), this.Override, this.Custom, this.Location, diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/InstallCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/InstallCommand.cs @@ -104,52 +104,5 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common return options; } - - /// <summary> - /// Registers callbacks on an asynchronous operation and waits for the results. - /// </summary> - /// <param name="operation">The asynchronous operation.</param> - /// <param name="activity">A <see cref="string" /> instance.</param> - /// <returns>A <see cref="InstallResult" /> instance.</returns> - protected InstallResult RegisterCallbacksAndWait( - IAsyncOperationWithProgress<InstallResult, InstallProgress> operation, - string activity) - { - var activityId = this.GetNewProgressActivityId(); - WriteProgressAdapter adapter = new (this); - operation.Progress = (context, progress) => - { - ProgressRecord record = new (activityId, activity, progress.State.ToString()) - { - RecordType = ProgressRecordType.Processing, - }; - - if (progress.State == PackageInstallProgressState.Downloading && progress.BytesRequired != 0) - { - record.StatusDescription = $"{progress.BytesDownloaded / 1000000.0f:0.0} MB / {progress.BytesRequired / 1000000.0f:0.0} MB"; - record.PercentComplete = (int)(progress.DownloadProgress * 100); - } - else if (progress.State == PackageInstallProgressState.Installing) - { - record.PercentComplete = (int)(progress.InstallationProgress * 100); - } - - adapter.WriteProgress(record); - }; - operation.Completed = (context, status) => - { - adapter.WriteProgress(new ProgressRecord(activityId, activity, status.ToString()) - { - RecordType = ProgressRecordType.Completed, - }); - adapter.Completed = true; - }; - System.Console.CancelKeyPress += (sender, e) => - { - operation.Cancel(); - }; - adapter.Wait(); - return operation.GetResults(); - } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/ManagementDeploymentCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/ManagementDeploymentCommand.cs @@ -10,11 +10,11 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common using System.Collections.Generic; using System.Management.Automation; using System.Runtime.InteropServices; + using System.Threading.Tasks; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Common; using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Helpers; - using Microsoft.WinGet.Resources; /// <summary> /// This is the base class for all of the commands in this module that use the COM APIs. @@ -41,7 +41,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common } /// <summary> - /// Executes the cmdlet. All cmdlets that uses the COM APIs MUST use this method. + /// Executes the cmdlet. All cmdlets that uses the COM APIs and don't call async functions MUST use this method. /// The inproc COM API may deadlock on an STA thread. /// </summary> /// <typeparam name="TResult">The type of result of the cmdlet.</typeparam> @@ -58,6 +58,24 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common } /// <summary> + /// Executes the cmdlet in a different thread and waits for results. + /// </summary> + /// <typeparam name="TResult">The type of result of the cmdlet.</typeparam> + /// <param name="func">Cmdlet function.</param> + /// <returns>The result of the cmdlet.</returns> + protected TResult Execute<TResult>(Func<Task<TResult>> func) + { + var runningTask = this.RunOnMTA( + async () => + { + return await func(); + }); + + this.Wait(runningTask); + return runningTask.Result; + } + + /// <summary> /// Retrieves the specified source or all sources if <paramref name="source" /> is null. /// </summary> /// <returns>A list of <see cref="PackageCatalogReference" /> instances.</returns> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs @@ -9,6 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common using System; using System.Collections.Generic; using System.Management.Automation; + using System.Threading.Tasks; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Extensions; @@ -56,17 +57,17 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common /// <param name="match">The match option.</param> /// <param name="callback">The method to call after retrieving the package and version to operate upon.</param> /// <returns>Result of the callback.</returns> - protected TResult? GetPackageAndExecute<TResult>( + protected async Task<TResult?> GetPackageAndExecuteAsync<TResult>( CompositeSearchBehavior behavior, PackageFieldMatchOption match, - Func<CatalogPackage, PackageVersionId?, TResult> callback) + Func<CatalogPackage, PackageVersionId?, Task<TResult>> callback) where TResult : class { CatalogPackage package = this.GetCatalogPackage(behavior, match); PackageVersionId? version = this.GetPackageVersionId(package); if (this.ShouldProcess(package.ToString(version))) { - return callback(package, version); + return await callback(package, version); } return null; diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs @@ -7,6 +7,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands { using System.Management.Automation; + using System.Threading.Tasks; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Helpers; @@ -23,7 +24,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// Initializes a new instance of the <see cref="InstallerPackageCommand"/> class. /// </summary> /// <param name="psCmdlet">Caller cmdlet.</param> - /// <param name="psInstallMode">Install mode to use.</param> /// <param name="override">Override arguments to be passed on to the installer.</param> /// <param name="custom">Additional arguments.</param> /// <param name="location">Installation location.</param> @@ -40,7 +40,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="query">Match against any field of a package.</param> public InstallerPackageCommand( PSCmdlet psCmdlet, - string psInstallMode, string @override, string custom, string location, @@ -96,10 +95,10 @@ namespace Microsoft.WinGet.Client.Engine.Commands string psPackageInstallMode) { var result = this.Execute( - () => this.GetPackageAndExecute( + async () => await this.GetPackageAndExecuteAsync( CompositeSearchBehavior.RemotePackagesFromRemoteCatalogs, PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), - (package, version) => + async (package, version) => { InstallOptions options = this.GetInstallOptions(version, psPackageInstallMode); if (psProcessorArchitecture != "Default") @@ -110,7 +109,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands options.PackageInstallScope = PSEnumHelpers.ToPackageInstallScope(psPackageInstallScope); - return this.InstallPackage(package, options); + return await this.InstallPackageAsync(package, options); })); if (result != null) @@ -131,14 +130,14 @@ namespace Microsoft.WinGet.Client.Engine.Commands string psPackageInstallMode) { var result = this.Execute( - () => this.GetPackageAndExecute( + async () => await this.GetPackageAndExecuteAsync( CompositeSearchBehavior.LocalCatalogs, PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), - (package, version) => + async (package, version) => { InstallOptions options = this.GetInstallOptions(version, psPackageInstallMode); options.AllowUpgradeToUnknownVersion = includeUnknown; - return this.UpgradePackage(package, options); + return await this.UpgradePackageAsync(package, options); })); if (result != null) @@ -147,26 +146,26 @@ namespace Microsoft.WinGet.Client.Engine.Commands } } - private InstallResult InstallPackage( + private async Task<InstallResult> InstallPackageAsync( CatalogPackage package, InstallOptions options) { - var operation = PackageManagerWrapper.Instance.InstallPackageAsync(package, options); - return this.RegisterCallbacksAndWait(operation, string.Format( - Resources.ProgressRecordActivityInstalling, - package.Name)); + var installOperation = new InstallOperationWithProgress( + this, + string.Format(Resources.ProgressRecordActivityInstalling, package.Name)); + return await installOperation.ExecuteAsync( + () => PackageManagerWrapper.Instance.InstallPackageAsync(package, options)); } - private InstallResult UpgradePackage( + private async Task<InstallResult> UpgradePackageAsync( CatalogPackage package, InstallOptions options) { - var operation = PackageManagerWrapper.Instance.UpgradePackageAsync(package, options); - return this.RegisterCallbacksAndWait( - operation, - string.Format( - Resources.ProgressRecordActivityUpdating, - package.Name)); + var installOperation = new InstallOperationWithProgress( + this, + string.Format(Resources.ProgressRecordActivityUpdating, package.Name)); + return await installOperation.ExecuteAsync( + () => PackageManagerWrapper.Instance.UpgradePackageAsync(package, options)); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UninstallPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UninstallPackageCommand.cs @@ -6,8 +6,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands { - using System; using System.Management.Automation; + using System.Threading.Tasks; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Helpers; @@ -73,13 +73,13 @@ namespace Microsoft.WinGet.Client.Engine.Commands bool force) { var result = this.Execute( - () => this.GetPackageAndExecute( + async () => await this.GetPackageAndExecuteAsync( CompositeSearchBehavior.LocalCatalogs, PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), - (package, version) => + async (package, version) => { UninstallOptions options = this.GetUninstallOptions(version, PSEnumHelpers.ToPackageUninstallMode(psPackageUninstallMode), force); - return this.UninstallPackage(package, options); + return await this.UninstallPackageAsync(package, options); })); if (result != null) @@ -110,39 +110,15 @@ namespace Microsoft.WinGet.Client.Engine.Commands return options; } - private UninstallResult UninstallPackage( + private async Task<UninstallResult> UninstallPackageAsync( CatalogPackage package, UninstallOptions options) { - string activity = string.Format( - Resources.ProgressRecordActivityUninstalling, - package.Name); - - var operation = PackageManagerWrapper.Instance.UninstallPackageAsync(package, options); - - var activityId = this.GetNewProgressActivityId(); - WriteProgressAdapter adapter = new (this); - operation.Progress = (context, progress) => - { - adapter.WriteProgress(new ProgressRecord(activityId, activity, progress.State.ToString()) - { - RecordType = ProgressRecordType.Processing, - }); - }; - operation.Completed = (context, status) => - { - adapter.WriteProgress(new ProgressRecord(activityId, activity, status.ToString()) - { - RecordType = ProgressRecordType.Completed, - }); - adapter.Completed = true; - }; - Console.CancelKeyPress += (sender, e) => - { - operation.Cancel(); - }; - adapter.Wait(); - return operation.GetResults(); + var progressOperation = new UninstallOperationWithProgress( + this, + string.Format(Resources.ProgressRecordActivityUninstalling, package.Name)); + return await progressOperation.ExecuteAsync( + () => PackageManagerWrapper.Instance.UninstallPackageAsync(package, options)); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -9,6 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands using System; using System.Collections.Generic; using System.Management.Automation; + using System.Threading.Tasks; using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Common; using Microsoft.WinGet.Client.Engine.Exceptions; @@ -39,9 +40,16 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="preRelease">Use prerelease version on GitHub.</param> public void AssertUsingLatest(bool preRelease) { - var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); - string expectedVersion = gitHubClient.GetLatestVersionTagName(preRelease); - this.Assert(expectedVersion); + var runningTask = this.RunOnMTA( + async () => + { + var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + string expectedVersion = await gitHubClient.GetLatestReleaseTagNameAsync(preRelease); + this.Assert(expectedVersion); + return true; + }); + + this.Wait(runningTask); } /// <summary> @@ -58,11 +66,20 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// </summary> /// <param name="preRelease">Use prerelease version on GitHub.</param> /// <param name="allUsers">Install for all users. Requires admin.</param> - public void RepairUsingLatest(bool preRelease, bool allUsers) + /// <param name="force">Force application shutdown.</param> + public void RepairUsingLatest(bool preRelease, bool allUsers, bool force) { - var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); - string expectedVersion = gitHubClient.GetLatestVersionTagName(preRelease); - this.Repair(expectedVersion, allUsers); + this.ValidateWhenAllUsers(allUsers); + var runningTask = this.RunOnMTA( + async () => + { + var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + string expectedVersion = await gitHubClient.GetLatestReleaseTagNameAsync(preRelease); + await this.RepairStateMachineAsync(expectedVersion, allUsers, force); + return true; + }); + + this.Wait(runningTask); } /// <summary> @@ -70,31 +87,29 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// </summary> /// <param name="expectedVersion">The expected version, if any.</param> /// <param name="allUsers">Install for all users. Requires admin.</param> - public void Repair(string expectedVersion, bool allUsers) + /// <param name="force">Force application shutdown.</param> + public void Repair(string expectedVersion, bool allUsers, bool force) { - if (allUsers) - { - if (Utilities.ExecutingAsSystem) - { - throw new NotSupportedException(); - } - - if (!Utilities.ExecutingAsAdministrator) + this.ValidateWhenAllUsers(allUsers); + var runningTask = this.RunOnMTA( + async () => { - throw new WinGetRepairException(Resources.RepairAllUsersMessage); - } - } - - this.RepairStateMachine(expectedVersion, allUsers); + await this.RepairStateMachineAsync(expectedVersion, allUsers, force); + return true; + }); + this.Wait(runningTask); } - private void RepairStateMachine(string expectedVersion, bool allUsers) + private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers, bool force) { var seenCategories = new HashSet<IntegrityCategory>(); + var cancellationToken = this.GetCancellationToken(); var currentCategory = IntegrityCategory.Unknown; while (currentCategory != IntegrityCategory.Installed) { + cancellationToken.ThrowIfCancellationRequested(); + try { WinGetIntegrity.AssertWinGet(this, expectedVersion); @@ -117,7 +132,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands switch (currentCategory) { case IntegrityCategory.UnexpectedVersion: - this.InstallDifferentVersion(new WinGetVersion(expectedVersion), allUsers); + await this.InstallDifferentVersionAsync(new WinGetVersion(expectedVersion), allUsers, force); break; case IntegrityCategory.NotInPath: this.RepairEnvPath(); @@ -128,13 +143,13 @@ namespace Microsoft.WinGet.Client.Engine.Commands case IntegrityCategory.AppInstallerNotInstalled: case IntegrityCategory.AppInstallerNotSupported: case IntegrityCategory.Failure: - this.Install(expectedVersion, allUsers); + await this.InstallAsync(expectedVersion, allUsers, force); break; case IntegrityCategory.AppInstallerNoLicense: // This requires -AllUsers in admin mode. if (allUsers && Utilities.ExecutingAsAdministrator) { - this.Install(expectedVersion, allUsers); + await this.InstallAsync(expectedVersion, allUsers, force); } else { @@ -152,7 +167,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands } } - private void InstallDifferentVersion(WinGetVersion toInstallVersion, bool allUsers) + private async Task InstallDifferentVersionAsync(WinGetVersion toInstallVersion, bool allUsers, bool force) { var installedVersion = WinGetVersion.InstalledWinGetVersion; bool isDowngrade = installedVersion.CompareAsDeployment(toInstallVersion) > 0; @@ -164,10 +179,10 @@ namespace Microsoft.WinGet.Client.Engine.Commands StreamType.Verbose, message); var appxModule = new AppxModuleHelper(this); - appxModule.InstallFromGitHubRelease(toInstallVersion.TagVersion, allUsers, isDowngrade); + await appxModule.InstallFromGitHubReleaseAsync(toInstallVersion.TagVersion, allUsers, isDowngrade, force); } - private void Install(string toInstallVersion, bool allUsers) + private async Task InstallAsync(string toInstallVersion, bool allUsers, bool force) { // If we are here and toInstallVersion 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 @@ -175,11 +190,11 @@ namespace Microsoft.WinGet.Client.Engine.Commands if (string.IsNullOrEmpty(toInstallVersion)) { var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); - toInstallVersion = gitHubClient.GetLatestVersionTagName(false); + toInstallVersion = await gitHubClient.GetLatestReleaseTagNameAsync(false); } var appxModule = new AppxModuleHelper(this); - appxModule.InstallFromGitHubRelease(toInstallVersion, allUsers, false); + await appxModule.InstallFromGitHubReleaseAsync(toInstallVersion, allUsers, false, force); } private void Register() @@ -201,5 +216,21 @@ namespace Microsoft.WinGet.Client.Engine.Commands this.Write(StreamType.Verbose, $"PATH environment variable updated"); } + + private void ValidateWhenAllUsers(bool allUsers) + { + if (allUsers) + { + if (Utilities.ExecutingAsSystem) + { + throw new NotSupportedException(); + } + + if (!Utilities.ExecutingAsAdministrator) + { + throw new WinGetRepairException(Resources.RepairAllUsersMessage); + } + } + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Constants.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Constants.cs @@ -35,6 +35,11 @@ namespace Microsoft.WinGet.Client.Engine.Common public const string PathEnvVar = "PATH"; /// <summary> + /// One MB. + /// </summary> + public const int OneMB = 1024 * 1024; + + /// <summary> /// Repository owners. /// </summary> public class RepositoryOwner diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -84,24 +84,32 @@ namespace Microsoft.WinGet.Client.Engine.Common private static IntegrityCategory GetReason(PowerShellCmdlet pwshCmdlet) { // Ok, so you are here because calling winget --version failed. Lets try to figure out why. - - // When running winget.exe on PowerShell the message of the Win32Exception will distinguish between - // 'The system cannot find the file specified' and 'No applicable app licenses found' but of course - // the HRESULT is the same (E_FAIL). - // To not compare strings let Powershell handle it. If calling winget throws an - // ApplicationFailedException then is most likely that the license is not there. - try - { - var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); - ps.AddCommand("winget").Invoke(); - } - catch (ApplicationFailedException e) + var category = IntegrityCategory.Unknown; + pwshCmdlet.ExecuteInPowerShellThread(() => { - pwshCmdlet.Write(StreamType.Verbose, e.Message); - return IntegrityCategory.AppInstallerNoLicense; - } - catch (Exception) + // When running winget.exe on PowerShell the message of the Win32Exception will distinguish between + // 'The system cannot find the file specified' and 'No applicable app licenses found' but of course + // the HRESULT is the same (E_FAIL). + // To not compare strings let Powershell handle it. If calling winget throws an + // ApplicationFailedException then is most likely that the license is not there. + try + { + var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); + ps.AddCommand("winget").Invoke(); + } + catch (ApplicationFailedException e) + { + pwshCmdlet.Write(StreamType.Verbose, e.Message); + category = IntegrityCategory.AppInstallerNoLicense; + } + catch (Exception) + { + } + }); + + if (category != IntegrityCategory.Unknown) { + return category; } // First lets check if the file is there, which means it is installed or someone is taking our place. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Extensions/ReleaseExtensions.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Extensions/ReleaseExtensions.cs @@ -0,0 +1,55 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ReleaseExtensions.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Extensions +{ + using System.Linq; + using Microsoft.WinGet.Client.Engine.Exceptions; + using Microsoft.WinGet.Resources; + using Octokit; + + /// <summary> + /// Extension methods for Octokit.Release. + /// </summary> + internal static class ReleaseExtensions + { + /// <summary> + /// Gets the Asset. + /// </summary> + /// <param name="release">GitHub release.</param> + /// <param name="name">Name of asset.</param> + /// <returns>The asset.</returns> + public static ReleaseAsset GetAsset(this Release release, string name) + { + var assets = release.Assets.Where(a => a.Name == name); + + if (assets.Any()) + { + return assets.First(); + } + + throw new WinGetRepairException(string.Format(Resources.ReleaseAssetNotFound, name)); + } + + /// <summary> + /// Gets the asset that ends with the string. + /// </summary> + /// <param name="release">GitHub release.</param> + /// <param name="name">Asset last part name.</param> + /// <returns>The asset.</returns> + public static ReleaseAsset GetAssetEndsWith(this Release release, string name) + { + var assets = release.Assets.Where(a => a.Name.EndsWith(name)); + + if (assets.Any()) + { + return assets.First(); + } + + throw new WinGetRepairException(string.Format(Resources.ReleaseAssetNotFound, name)); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs @@ -12,8 +12,11 @@ namespace Microsoft.WinGet.Client.Engine.Helpers using System.Linq; using System.Management.Automation; using System.Runtime.InteropServices; + using System.Threading.Tasks; using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Client.Engine.Extensions; using Microsoft.WinGet.Common.Command; + using Octokit; using static Microsoft.WinGet.Client.Engine.Common.Constants; /// <summary> @@ -26,6 +29,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private const string GetAppxPackage = "Get-AppxPackage"; private const string AddAppxPackage = "Add-AppxPackage"; private const string AddAppxProvisionedPackage = "Add-AppxProvisionedPackage"; + private const string GetCommand = "Get-Command"; // Parameters name private const string Name = "Name"; @@ -34,18 +38,22 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private const string WarningAction = "WarningAction"; private const string PackagePath = "PackagePath"; private const string LicensePath = "LicensePath"; + private const string Module = "Module"; + private const string StubPackageOption = "StubPackageOption"; // Parameter Values private const string Appx = "Appx"; private const string Stop = "Stop"; private const string SilentlyContinue = "SilentlyContinue"; private const string Online = "Online"; + private const string UsePreference = "UsePreference"; // Options private const string UseWindowsPowerShell = "UseWindowsPowerShell"; private const string ForceUpdateFromAnyVersion = "ForceUpdateFromAnyVersion"; private const string Register = "Register"; private const string DisableDevelopmentMode = "DisableDevelopmentMode"; + private const string ForceTargetApplicationShutdown = "ForceTargetApplicationShutdown"; private const string AppInstallerName = "Microsoft.DesktopAppInstaller"; private const string AppxManifest = "AppxManifest.xml"; @@ -73,6 +81,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private const string XamlAssetArm64 = "Microsoft.UI.Xaml.2.7.arm64.appx"; private readonly PowerShellCmdlet pwshCmdlet; + private readonly HttpClientHelper httpClientHelper; /// <summary> /// Initializes a new instance of the <see cref="AppxModuleHelper"/> class. @@ -81,6 +90,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) { this.pwshCmdlet = pwshCmdlet; + this.httpClientHelper = new HttpClientHelper(); } /// <summary> @@ -149,55 +159,63 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <param name="releaseTag">Release tag of GitHub release.</param> /// <param name="allUsers">If install for all users is needed.</param> /// <param name="isDowngrade">Is downgrade.</param> - public void InstallFromGitHubRelease(string releaseTag, bool allUsers, bool isDowngrade) + /// <param name="force">Force application shutdown.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + public async Task InstallFromGitHubReleaseAsync(string releaseTag, bool allUsers, bool isDowngrade, bool force) { - this.InstallDependencies(); + await this.InstallDependenciesAsync(); if (isDowngrade) { // Add-AppxProvisionedPackage doesn't support downgrade. - this.AddAppInstallerBundle(releaseTag, true); + await this.AddAppInstallerBundleAsync(releaseTag, true, force); if (allUsers) { - this.AddProvisionPackage(releaseTag); + await this.AddProvisionPackageAsync(releaseTag); } } else { if (allUsers) { - this.AddProvisionPackage(releaseTag); + await this.AddProvisionPackageAsync(releaseTag); } else { - this.AddAppInstallerBundle(releaseTag, false); + await this.AddAppInstallerBundleAsync(releaseTag, false, force); } } } - private void AddProvisionPackage(string releaseTag) + private async Task AddProvisionPackageAsync(string releaseTag) { var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); - var release = githubClient.GetRelease(releaseTag); + var release = await githubClient.GetReleaseAsync(releaseTag); - using var bundleFile = new TempFile(); - var bundleAsset = release.Assets.Where(a => a.Name == MsixBundleName).First(); - githubClient.DownloadUrl(bundleAsset.BrowserDownloadUrl, bundleFile.FullPath); + var bundleAsset = release.GetAsset(MsixBundleName); + using var bundleFile = new TempFile(fileName: MsixBundleName); + await this.httpClientHelper.DownloadUrlWithProgressAsync( + bundleAsset.BrowserDownloadUrl, bundleFile.FullPath, this.pwshCmdlet); - using var licenseFile = new TempFile(); - var licenseAsset = release.Assets.Where(a => a.Name.EndsWith(License)).First(); - githubClient.DownloadUrl(licenseAsset.BrowserDownloadUrl, licenseFile.FullPath); + var licenseAsset = release.GetAssetEndsWith(License); + using var licenseFile = new TempFile(fileName: licenseAsset.Name); + await this.httpClientHelper.DownloadUrlWithProgressAsync( + licenseAsset.BrowserDownloadUrl, licenseFile.FullPath, this.pwshCmdlet); try { - var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); - ps.AddCommand(AddAppxProvisionedPackage) - .AddParameter(Online) - .AddParameter(PackagePath, bundleFile.FullPath) - .AddParameter(LicensePath, licenseFile.FullPath) - .AddParameter(ErrorAction, Stop) - .Invoke(); + this.pwshCmdlet.ExecuteInPowerShellThread( + () => + { + var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); + ps.AddCommand(AddAppxProvisionedPackage) + .AddParameter(Online) + .AddParameter(PackagePath, bundleFile.FullPath) + .AddParameter(LicensePath, licenseFile.FullPath) + .AddParameter(ErrorAction, Stop) + .Invoke(); + }); } catch (RuntimeException e) { @@ -206,7 +224,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } } - private void AddAppInstallerBundle(string releaseTag, bool downgrade) + private async Task AddAppInstallerBundleAsync(string releaseTag, bool downgrade, bool force) { var options = new List<string>(); if (downgrade) @@ -214,14 +232,24 @@ namespace Microsoft.WinGet.Client.Engine.Helpers options.Add(ForceUpdateFromAnyVersion); } + if (force) + { + options.Add(ForceTargetApplicationShutdown); + } + + var parameters = new Dictionary<string, object>(); + if (this.IsStubPackageOptionPresent()) + { + parameters.Add(StubPackageOption, UsePreference); + } + try { var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); - var release = githubClient.GetRelease(releaseTag); + var release = await githubClient.GetReleaseAsync(releaseTag); - using var bundleFile = new TempFile(); - var bundleAsset = release.Assets.Where(a => a.Name == MsixBundleName).First(); - this.AddAppxPackageAsUri(bundleAsset.BrowserDownloadUrl, options); + var bundleAsset = release.GetAsset(MsixBundleName); + await this.AddAppxPackageAsUriAsync(bundleAsset.BrowserDownloadUrl, MsixBundleName, parameters, options); } catch (RuntimeException e) { @@ -241,7 +269,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers .FirstOrDefault(); } - private void InstallDependencies() + private async Task InstallDependenciesAsync() { // A better implementation would use Add-AppxPackage with -DependencyPath, but // the Appx module needs to be remoted into Windows PowerShell. When the string[] parameter @@ -249,11 +277,11 @@ namespace Microsoft.WinGet.Client.Engine.Helpers // Here we should: if we are in Windows Powershell then run Add-AppxPackage with -DependencyPath // if we are in Core, then start powershell.exe and run the same command. Right now, we just // do Add-AppxPackage for each one. - this.InstallVCLibsDependencies(); - this.InstallUiXaml(); + await this.InstallVCLibsDependenciesAsync(); + await this.InstallUiXamlAsync(); } - private void InstallVCLibsDependencies() + private async Task InstallVCLibsDependenciesAsync() { var result = this.ExecuteAppxCmdlet( GetAppxPackage, @@ -319,7 +347,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers foreach (var vclib in vcLibsDependencies) { - this.AddAppxPackageAsUri(vclib); + await this.AddAppxPackageAsUriAsync(vclib, vclib.Substring(vclib.LastIndexOf('/') + 1)); } } else @@ -328,32 +356,32 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } } - private void InstallUiXaml() + private async Task InstallUiXamlAsync() { var uiXamlObjs = this.GetAppxObject(XamlPackage27); if (uiXamlObjs is null) { var githubRelease = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.UiXaml); - var xamlRelease = githubRelease.GetRelease(XamlReleaseTag273); + var xamlRelease = await githubRelease.GetReleaseAsync(XamlReleaseTag273); - var packagesToInstall = new List<string>(); + var packagesToInstall = new List<ReleaseAsset>(); var arch = RuntimeInformation.OSArchitecture; if (arch == Architecture.X64) { - packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetX64).First().BrowserDownloadUrl); + packagesToInstall.Add(xamlRelease.GetAsset(XamlAssetX64)); } else if (arch == Architecture.X86) { - packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetX86).First().BrowserDownloadUrl); + packagesToInstall.Add(xamlRelease.GetAsset(XamlAssetX86)); } else if (arch == Architecture.Arm64) { // Deployment please figure out for me. - packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetX64).First().BrowserDownloadUrl); - packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetX86).First().BrowserDownloadUrl); - packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetArm).First().BrowserDownloadUrl); - packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetArm64).First().BrowserDownloadUrl); + packagesToInstall.Add(xamlRelease.GetAsset(XamlAssetX64)); + packagesToInstall.Add(xamlRelease.GetAsset(XamlAssetX86)); + packagesToInstall.Add(xamlRelease.GetAsset(XamlAssetArm)); + packagesToInstall.Add(xamlRelease.GetAsset(XamlAssetArm64)); } else { @@ -362,22 +390,32 @@ namespace Microsoft.WinGet.Client.Engine.Helpers foreach (var package in packagesToInstall) { - this.AddAppxPackageAsUri(package); + await this.AddAppxPackageAsUriAsync(package.BrowserDownloadUrl, package.Name); } } } - private void AddAppxPackageAsUri(string packageUri, IList<string>? options = null) + private async Task AddAppxPackageAsUriAsync(string packageUri, string fileName, Dictionary<string, object>? parameters = null, IList<string>? options = null) { try { + var thisParams = new Dictionary<string, object> + { + { Path, packageUri }, + { ErrorAction, Stop }, + }; + + if (parameters != null) + { + foreach (var param in parameters) + { + thisParams.Add(param.Key, param.Value); + } + } + _ = this.ExecuteAppxCmdlet( AddAppxPackage, - new Dictionary<string, object> - { - { Path, packageUri }, - { ErrorAction, Stop }, - }, + thisParams, options); } catch (RuntimeException e) @@ -386,7 +424,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers if (e.ErrorRecord.CategoryInfo.Category == ErrorCategory.OpenError) { this.pwshCmdlet.Write(StreamType.Verbose, $"Failed adding package [{packageUri}]. Retrying downloading it."); - this.DownloadPackageAndAdd(packageUri, options); + await this.DownloadPackageAndAddAsync(packageUri, fileName, options); } else { @@ -396,13 +434,11 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } } - private void DownloadPackageAndAdd(string packageUrl, IList<string>? options) + private async Task DownloadPackageAndAddAsync(string packageUrl, string fileName, IList<string>? options) { - using var tempFile = new TempFile(); + using var tempFile = new TempFile(fileName: fileName); - // This is weird but easy. - var githubRelease = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); - githubRelease.DownloadUrl(packageUrl, tempFile.FullPath); + await this.httpClientHelper.DownloadUrlWithProgressAsync(packageUrl, tempFile.FullPath, this.pwshCmdlet); _ = this.ExecuteAppxCmdlet( AddAppxPackage, @@ -416,46 +452,81 @@ namespace Microsoft.WinGet.Client.Engine.Helpers 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' + Collection<PSObject> result = new Collection<PSObject>(); + + this.pwshCmdlet.ExecuteInPowerShellThread( + () => + { + 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(); + ps.AddCommand(ImportModule) + .AddParameter(Name, Appx) + .AddParameter(UseWindowsPowerShell) + .AddParameter(WarningAction, SilentlyContinue) + .AddStatement(); #endif - string cmd = cmdlet; - ps.AddCommand(cmdlet); + string cmd = cmdlet; + ps.AddCommand(cmdlet); - if (parameters != null) - { - foreach (var p in parameters) - { - cmd += $" -{p.Key} {p.Value}"; - } + if (parameters != null) + { + foreach (var p in parameters) + { + cmd += $" -{p.Key} {p.Value}"; + } - ps.AddParameters(parameters); - } + ps.AddParameters(parameters); + } - if (options != null) - { - foreach (var option in options) + if (options != null) + { + foreach (var option in options) + { + cmd += $" -{option}"; + ps.AddParameter(option); + } + } + + this.pwshCmdlet.Write(StreamType.Verbose, $"Executing Appx cmdlet {cmd}"); + result = ps.Invoke(); + }); + + return result; + } + + private bool IsStubPackageOptionPresent() + { + bool result = false; + this.pwshCmdlet.ExecuteInPowerShellThread( + () => { - cmd += $" -{option}"; - ps.AddParameter(option); - } - } + var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); + +#if !POWERSHELL_WINDOWS + ps.AddCommand(ImportModule) + .AddParameter(Name, Appx) + .AddParameter(UseWindowsPowerShell) + .AddParameter(WarningAction, SilentlyContinue) + .AddStatement(); +#endif + + var cmdInfo = ps.AddCommand(GetCommand) + .AddParameter(Name, AddAppxPackage) + .AddParameter(Module, Appx) + .Invoke<CommandInfo>() + .FirstOrDefault(); + + result = cmdInfo != null && cmdInfo.Parameters.ContainsKey(StubPackageOption); + }); - this.pwshCmdlet.Write(StreamType.Verbose, $"Executing Appx cmdlet {cmd}"); - var result = ps.Invoke(); return result; } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs @@ -6,24 +6,16 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { - using System; - using System.Collections.Generic; - using System.IO; using System.Threading.Tasks; using Octokit; - using FileMode = System.IO.FileMode; /// <summary> /// Handles GitHub interactions. /// </summary> internal class GitHubClient { - private const string UserAgent = "winget-powershell"; - private const string ContentType = "application/octet-stream"; - private readonly string owner; private readonly string repo; - private readonly IGitHubClient gitHubClient; /// <summary> @@ -33,7 +25,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <param name="repo">Repository.</param> public GitHubClient(string owner, string repo) { - this.gitHubClient = new Octokit.GitHubClient(new ProductHeaderValue(UserAgent)); + this.gitHubClient = new Octokit.GitHubClient(new ProductHeaderValue(HttpClientHelper.UserAgent)); this.owner = owner; this.repo = repo; } @@ -43,58 +35,19 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// </summary> /// <param name="releaseTag">Release tag.</param> /// <returns>The Release.</returns> - public Release GetRelease(string releaseTag) - { - return this.GetReleaseAsync(releaseTag).GetAwaiter().GetResult(); - } - - /// <summary> - /// Gets a release. - /// </summary> - /// <param name="releaseTag">Release tag.</param> - /// <returns>The Release.</returns> public async Task<Release> GetReleaseAsync(string releaseTag) { return await this.gitHubClient.Repository.Release.Get(this.owner, this.repo, releaseTag); } /// <summary> - /// Gets the latest released version and waits. + /// Gets the latest released and waits. /// </summary> /// <param name="includePreRelease">Include prerelease.</param> /// <returns>Latest version.</returns> - public string GetLatestVersionTagName(bool includePreRelease) + public async Task<string> GetLatestReleaseTagNameAsync(bool includePreRelease) { - return this.GetLatestVersionAsync(includePreRelease).GetAwaiter().GetResult().TagName; - } - - /// <summary> - /// Downloads a file from a url and waits. - /// </summary> - /// <param name="url">Url.</param> - /// <param name="fileName">File name.</param> - public void DownloadUrl(string url, string fileName) - { - this.DownloadUrlAsync(url, fileName).GetAwaiter().GetResult(); - } - - /// <summary> - /// Downloads a file from a url. - /// </summary> - /// <param name="url">Url.</param> - /// <param name="fileName">File name.</param> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - public async Task DownloadUrlAsync(string url, string fileName) - { - var response = await this.gitHubClient.Connection.Get<object>( - new Uri(url), - new Dictionary<string, string>(), - ContentType); - - using var memoryStream = new MemoryStream((byte[])response.Body); - using var fileStream = File.Open(fileName, FileMode.OpenOrCreate); - memoryStream.Position = 0; - await memoryStream.CopyToAsync(fileStream); + return (await this.GetLatestReleaseAsync(includePreRelease)).TagName; } /// <summary> @@ -102,7 +55,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// </summary> /// <param name="includePreRelease">Include prerelease.</param> /// <returns>Latest version.</returns> - internal async Task<Release> GetLatestVersionAsync(bool includePreRelease) + public async Task<Release> GetLatestReleaseAsync(bool includePreRelease) { Release release; diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/HttpClientHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/HttpClientHelper.cs @@ -0,0 +1,115 @@ +// ----------------------------------------------------------------------------- +// <copyright file="HttpClientHelper.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Management.Automation; + using System.Net.Http; + using System.Threading.Tasks; + using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; + + /// <summary> + /// Helper class for HttpClient calls. + /// </summary> + internal class HttpClientHelper + { + /// <summary> + /// The user agent of this module. + /// </summary> + public const string UserAgent = "winget-powershell"; + + private static readonly HttpClient Client; + + static HttpClientHelper() + { + Client = new HttpClient(); + } + + /// <summary> + /// Downloads a file from a url. + /// </summary> + /// <param name="url">Url.</param> + /// <param name="fileName">File name.</param> + /// /// <param name="pwshCmdlet">PowershellCmdlet.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + public async Task DownloadUrlWithProgressAsync(string url, string fileName, PowerShellCmdlet pwshCmdlet) + { + pwshCmdlet.Write(StreamType.Verbose, $"Downloading {url}"); + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("User-Agent", UserAgent); + + var cancellationToken = pwshCmdlet.GetCancellationToken(); + using var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + response.EnsureSuccessStatusCode(); + + try + { + long? contentLength = response.Content.Headers.ContentLength; + var responseStream = await response.Content.ReadAsStreamAsync(); + + using var fileStream = File.Open(fileName, FileMode.OpenOrCreate); + + if (contentLength.HasValue) + { + pwshCmdlet.Write(StreamType.Verbose, $"Size {contentLength} bytes"); + + byte[] buffer = new byte[Constants.OneMB]; + int bytesRead, totalBytes = 0; + + var activityId = pwshCmdlet.GetNewProgressActivityId(); + double lengthInMB = (double)contentLength.Value / Constants.OneMB; + try + { + int maxPercentComplete = 0; + while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) + { + await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken); + totalBytes += bytesRead; + + int percentComplete = (int)((double)totalBytes / contentLength * 100); + if (percentComplete > maxPercentComplete) + { + maxPercentComplete = percentComplete; + ProgressRecord record = new (activityId, url, Resources.DownloadingMessage) + { + RecordType = ProgressRecordType.Processing, + }; + + double progress = (double)totalBytes / Constants.OneMB; + record.StatusDescription = $"{progress:0.0} MB / {lengthInMB:0.0} MB"; + record.PercentComplete = percentComplete; + pwshCmdlet.Write(StreamType.Progress, record); + } + } + } + finally + { + pwshCmdlet.CompleteProgress(activityId, url, Resources.DownloadingMessage, true); + } + } + else + { + pwshCmdlet.Write(StreamType.Verbose, $"Content-Length not found in response"); + await responseStream.CopyToAsync(fileStream); + } + } + catch (Exception) + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + throw; + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/InstallOperationWithProgress.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/InstallOperationWithProgress.cs @@ -0,0 +1,53 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InstallOperationWithProgress.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System.Management.Automation; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Common.Command; + using Windows.Foundation; + + /// <summary> + /// Handlers install or update operations with progress. + /// </summary> + internal class InstallOperationWithProgress : OperationWithProgressBase<InstallResult, InstallProgress> + { + /// <summary> + /// Initializes a new instance of the <see cref="InstallOperationWithProgress"/> class. + /// </summary> + /// <param name="pwshCmdlet">A <see cref="PowerShellCmdlet" /> instance.</param> + /// <param name="activity">Activity.</param> + public InstallOperationWithProgress(PowerShellCmdlet pwshCmdlet, string activity) + : base(pwshCmdlet, activity) + { + } + + /// <inheritdoc/> + public override void Progress(IAsyncOperationWithProgress<InstallResult, InstallProgress> operation, InstallProgress progress) + { + ProgressRecord record = new (this.ActivityId, this.Activity, progress.State.ToString()) + { + RecordType = ProgressRecordType.Processing, + }; + + if (progress.State == PackageInstallProgressState.Downloading && progress.BytesRequired != 0) + { + double downloaded = (double)progress.BytesDownloaded / Constants.OneMB; + double total = (double)progress.BytesRequired / Constants.OneMB; + record.StatusDescription = $"{downloaded:0.0} MB / {total:0.0} MB"; + record.PercentComplete = (int)(progress.DownloadProgress * 100); + } + else if (progress.State == PackageInstallProgressState.Installing) + { + record.PercentComplete = (int)(progress.InstallationProgress * 100); + } + + this.PwshCmdlet.Write(StreamType.Progress, record); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/OperationWithProgressBase.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/OperationWithProgressBase.cs @@ -0,0 +1,87 @@ +// ----------------------------------------------------------------------------- +// <copyright file="OperationWithProgressBase.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.Threading.Tasks; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; + using Windows.Foundation; + + /// <summary> + /// Base class for async operations with progress. + /// </summary> + /// <typeparam name="TOperationResult">The operation result.</typeparam> + /// <typeparam name="TProgressData">Progress data.</typeparam> + internal abstract class OperationWithProgressBase<TOperationResult, TProgressData> + { + /// <summary> + /// Initializes a new instance of the <see cref="OperationWithProgressBase{TOperationResult, TProgressData}"/> class. + /// </summary> + /// <param name="pwshCmdlet">A <see cref="PowerShellCmdlet" /> instance.</param> + /// <param name="activity">Activity.</param> + public OperationWithProgressBase( + PowerShellCmdlet pwshCmdlet, + string activity) + { + this.PwshCmdlet = pwshCmdlet; + this.ActivityId = pwshCmdlet.GetNewProgressActivityId(); + this.Activity = activity; + } + + /// <summary> + /// Gets the PowerShellCmdlet. + /// </summary> + protected PowerShellCmdlet PwshCmdlet { get; } + + /// <summary> + /// Gets the progress activity id. + /// </summary> + protected int ActivityId { get; } + + /// <summary> + /// Gets the activity. + /// </summary> + protected string Activity { get; } + + /// <summary> + /// Progress callback. + /// </summary> + /// <param name="operation">Async operation in progress.</param> + /// <param name="progress">Progress data.</param> + public abstract void Progress(IAsyncOperationWithProgress<TOperationResult, TProgressData> operation, TProgressData progress); + + /// <summary> + /// Starts the operation and executes it as task. + /// Supports cancellation. + /// </summary> + /// <param name="func">Lambda with operation.</param> + /// <returns>TOperationReturn.</returns> + public async Task<TOperationResult> ExecuteAsync(Func<IAsyncOperationWithProgress<TOperationResult, TProgressData>> func) + { + var operation = func(); + operation.Progress = this.Progress; + + try + { + return await operation.AsTask(this.PwshCmdlet.GetCancellationToken()); + } + finally + { + this.Complete(); + } + } + + /// <summary> + /// Completes progress for this activity. + /// </summary> + protected virtual void Complete() + { + this.PwshCmdlet.CompleteProgress(this.ActivityId, this.Activity, Resources.Completed, true); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs @@ -34,14 +34,16 @@ namespace Microsoft.WinGet.Client.Engine.Helpers if (fileName is null) { this.FileName = Path.GetRandomFileName(); + this.FullPath = Path.Combine(Path.GetTempPath(), this.FileName); } else { this.FileName = fileName; + var randomDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(randomDir); + this.FullPath = Path.Combine(randomDir, this.FileName); } - this.FullPath = Path.Combine(Path.GetTempPath(), this.FileName); - if (deleteIfExists && File.Exists(this.FullPath)) { File.Delete(this.FullPath); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/UninstallOperationWithProgress.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/UninstallOperationWithProgress.cs @@ -0,0 +1,42 @@ +// ----------------------------------------------------------------------------- +// <copyright file="UninstallOperationWithProgress.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System.Management.Automation; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; + using Windows.Foundation; + + /// <summary> + /// Handler progress for uninstall. + /// </summary> + internal class UninstallOperationWithProgress : OperationWithProgressBase<UninstallResult, UninstallProgress> + { + /// <summary> + /// Initializes a new instance of the <see cref="UninstallOperationWithProgress"/> class. + /// </summary> + /// <param name="pwshCmdlet">A <see cref="PowerShellCmdlet" /> instance.</param> + /// <param name="activity">Activity.</param> + public UninstallOperationWithProgress(PowerShellCmdlet pwshCmdlet, string activity) + : base(pwshCmdlet, activity) + { + } + + /// <inheritdoc/> + public override void Progress(IAsyncOperationWithProgress<UninstallResult, UninstallProgress> operation, UninstallProgress progress) + { + ProgressRecord record = new (this.ActivityId, this.Activity, progress.State.ToString()) + { + RecordType = ProgressRecordType.Processing, + }; + record.StatusDescription = Resources.Uninstalling; + record.PercentComplete = (int)(progress.UninstallationProgress * 100); + this.PwshCmdlet.Write(StreamType.Progress, record); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WriteProgressAdapter.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WriteProgressAdapter.cs @@ -1,91 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="WriteProgressAdapter.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Client.Engine.Helpers -{ - using System.Collections.Generic; - using System.Management.Automation; - using System.Threading; - using Microsoft.WinGet.Common.Command; - - /// <summary> - /// Marshals calls to <see cref="Cmdlet.WriteProgress(ProgressRecord)" /> back to the main thread. - /// </summary> - internal class WriteProgressAdapter - { - private readonly AutoResetEvent resetEvent = new (false); - private readonly Queue<ProgressRecord> records = new (); - private readonly PowerShellCmdlet pwshCmdlet; - private volatile bool completed = false; - - /// <summary> - /// Initializes a new instance of the <see cref="WriteProgressAdapter" /> class. - /// </summary> - /// <param name="pwshCmdlet">A <see cref="PowerShellCmdlet" /> instance.</param> - public WriteProgressAdapter(PowerShellCmdlet pwshCmdlet) - { - this.pwshCmdlet = pwshCmdlet; - } - - /// <summary> - /// Sets a value indicating whether the asynchronous operation is finished and the main thread can continue. - /// </summary> - public bool Completed - { - set - { - this.completed = value; - if (value) - { - this.resetEvent.Set(); - } - } - } - - /// <summary> - /// This should be called on the main thread to wait for the asynchronous operation to complete. - /// </summary> - public void Wait() - { - while (!this.completed) - { - lock (this.records) - { - this.Flush(); - } - - this.resetEvent.WaitOne(); - } - - this.Flush(); - } - - /// <summary> - /// This is an analogue of the <see cref="Cmdlet.WriteProgress(ProgressRecord)" /> function. - /// </summary> - /// <param name="record">A <see cref="ProgressRecord" /> instance.</param> - public void WriteProgress(ProgressRecord record) - { - if (record != null) - { - lock (this.records) - { - this.records.Enqueue(record); - } - - this.resetEvent.Set(); - } - } - - private void Flush() - { - while (this.records.Count > 0) - { - this.pwshCmdlet.Write(StreamType.Progress, this.records.Dequeue()); - } - } - } -} 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 @@ -50,6 +50,7 @@ <PackageReference Include="Microsoft.CSharp" Version="4.7.0" Condition="'$(TargetFramework)' == '$(DesktopFramework)'" /> <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.4" Condition="'$(TargetFramework)' == '$(CoreFramework)'" /> <PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.22000.196" PrivateAssets="all" Condition="'$(TargetFramework)' == '$(DesktopFramework)'" /> + <PackageReference Include="System.Net.Http" Version="4.3.4" Condition="'$(TargetFramework)' == '$(DesktopFramework)'"/> </ItemGroup> <ItemGroup> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs @@ -70,6 +70,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to Completed. + /// </summary> + internal static string Completed { + get { + return ResourceManager.GetString("Completed", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Debug parameter not supported. /// </summary> internal static string DebugNotSupported { @@ -79,6 +88,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to Downloading. + /// </summary> + internal static string DownloadingMessage { + get { + return ResourceManager.GetString("DownloadingMessage", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to An error occurred while searching for packages: {0}. /// </summary> internal static string FindPackagesExceptionMessage { @@ -241,6 +259,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to Cannot find asset {0}. + /// </summary> + internal static string ReleaseAssetNotFound { + get { + return ResourceManager.GetString("ReleaseAssetNotFound", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Try running with -AllUsers in administrator mode.. /// </summary> internal static string RepairAllUsersHelpMessage { @@ -295,6 +322,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to Uninstalling. + /// </summary> + internal static string Uninstalling { + get { + return ResourceManager.GetString("Uninstalling", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to User settings file is invalid.. /// </summary> internal static string UserSettingsReadException { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx @@ -227,4 +227,17 @@ <data name="DebugNotSupported" xml:space="preserve"> <value>Debug parameter not supported</value> </data> + <data name="DownloadingMessage" xml:space="preserve"> + <value>Downloading</value> + </data> + <data name="Completed" xml:space="preserve"> + <value>Completed</value> + </data> + <data name="Uninstalling" xml:space="preserve"> + <value>Uninstalling</value> + </data> + <data name="ReleaseAssetNotFound" xml:space="preserve"> + <value>Cannot find asset {0}</value> + <comment>{Locked="{0}"} {0} - The asset name</comment> + </data> </root> \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/IAsyncOperationExtensions.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/IAsyncOperationExtensions.cs @@ -1,74 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="IAsyncOperationExtensions.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Configuration.Engine.Extensions -{ - using System.Threading; - using System.Threading.Tasks; - using Windows.Foundation; - - /// <summary> - /// Extension methods for IAsyncOperation objects. - /// </summary> - internal static class IAsyncOperationExtensions - { - /// <summary> - /// Wrap IAsyncOperationWithProgress into a task with cancellation support. - /// </summary> - /// <typeparam name="TOperationResult">The result of the operation.</typeparam> - /// <typeparam name="TProgressData">The progress data of the operation.</typeparam> - /// <param name="asyncOperation">The async operation.</param> - /// <param name="cancellationToken">Optional cancellation token.</param> - /// <returns>A task.</returns> - public static Task<TOperationResult> AsTask<TOperationResult, TProgressData>(this IAsyncOperationWithProgress<TOperationResult, TProgressData> asyncOperation, CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource<TOperationResult>(); - if (cancellationToken != default) - { - cancellationToken.Register(asyncOperation.Cancel); - } - - asyncOperation.Completed = (asyncInfo, asyncStatus) => - { - switch (asyncStatus) - { - case AsyncStatus.Canceled: - tcs.SetCanceled(); - break; - case AsyncStatus.Completed: - tcs.SetResult(asyncInfo.GetResults()); - break; - case AsyncStatus.Error: - tcs.SetException(asyncInfo.ErrorCode); - break; - case AsyncStatus.Started: - break; - default: - break; - } - }; - - // Make sure to throw operation cancelled exception if needed. - return tcs.Task.ContinueWith( - t => - { - if (t.IsCanceled) - { - cancellationToken.ThrowIfCancellationRequested(); - } - - if (!t.IsFaulted) - { - return t.Result; - } - - // If IsFaulted is true, the task's Status is equal to Faulted, - // and its Exception property will be non-null. - throw t.Exception!; - }); - } - } -} diff --git a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -196,11 +196,11 @@ if ($moduleToConfigure.HasFlag([ModuleType]::Configuration)) $additionalFiles = @( "Microsoft.Management.Configuration\Microsoft.Management.Configuration.dll" ) - $module.AddArchSpecificFiles($additionalFiles, "net6.0-windows10.0.22000.0\SharedDependencies", $BuildRoot, $Configuration) + $module.AddArchSpecificFiles($additionalFiles, "SharedDependencies", $BuildRoot, $Configuration) $additionalFiles = @( "Microsoft.Management.Configuration.Projection\net6.0-windows10.0.19041.0\Microsoft.Management.Configuration.Projection.dll" ) - $module.AddAnyCpuSpecificFilesToArch($additionalFiles, "net6.0-windows10.0.22000.0\SharedDependencies", $BuildRoot, $Configuration) + $module.AddAnyCpuSpecificFilesToArch($additionalFiles, "SharedDependencies", $BuildRoot, $Configuration) $modules += $module }