commit 09c87714da8cd08c4db884bd0c0bc4f56343e50a parent 28a30736d56da7926f8bcd4b1024a010c4fc2338 Author: Ruben Guerrero <rubengu@microsoft.com> Date: Fri, 10 Nov 2023 15:21:24 -0800 Allow Microsoft.WinGet.Client to run in any PowerShell session running as system (#3816) This PR adds support for running the Microsoft.WinGet.Client module in system context without the need to start pwsh.exe -MTA. Running as MTA is required using inproc Microsoft.Management.Deployment. If the module is running inproc and the current thread is not an MTA, it will create a new MTA thread and execute there. Otherwise, non inproc or already an MTA will use the current thread. This was done by sharing AsyncCommand (renamed to PowerShellCmdlet) from Microsoft.WinGet.Configuration. Originally, I wanted to create a new shared lib, but decided to just share the files between the projects. All cmdlets must inherit from PowerShellCmdlet. All cmdlets that use Microsoft.Management.Deployment must inherit ManagementDeploymentCommand and use Execute at the command engine entry point. As a safe mechanism, if any call to PackageManagerWrapper will verify the thread is not an STA if running inproc and fail. I verified the cmdlets work in system context locally. A future PR will add running our pester tests using psexe.exe Diffstat:
60 files changed, 921 insertions(+), 742 deletions(-)
diff --git a/src/PowerShell/CommonFiles/PowerShellCmdlet.cs b/src/PowerShell/CommonFiles/PowerShellCmdlet.cs @@ -0,0 +1,501 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PowerShellCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Common.Command +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Management.Automation; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.WinGet.Resources; + using Microsoft.WinGet.SharedLib.Exceptions; + using Microsoft.WinGet.SharedLib.PolicySettings; + + /// <summary> + /// This must be the base class for every cmdlet for winget PowerShell modules. + /// It supports: + /// - Async operations. + /// - Execute on an MTA. If the thread is already running on an MTA it will executed it, otherwise + /// it will create a new MTA thread. + /// Wait must be used to synchronously wait con the task. + /// </summary> + public abstract class PowerShellCmdlet + { + private const string Debug = "Debug"; + private static readonly string[] WriteInformationTags = new string[] { "PSHOST" }; + + private readonly PSCmdlet psCmdlet; + private readonly Thread originalThread; + + private readonly CancellationTokenSource source = new (); + private BlockingCollection<QueuedStream> queuedStreams = new (); + + private int progressActivityId = 0; + private ConcurrentDictionary<int, ProgressRecordType> progressRecords = new (); + + /// <summary> + /// Initializes a new instance of the <see cref="PowerShellCmdlet"/> class. + /// </summary> + /// <param name="psCmdlet">PSCmdlet.</param> + /// <param name="policies">Policies.</param> + public PowerShellCmdlet(PSCmdlet psCmdlet, HashSet<Policy> policies) + { + // Passing Debug will make all the message actions to be Inquire. For async operations + // and the current queue message implementation this doesn't make sense. + // PowerShell will inquire for any message giving the impression that the task is + // paused, but the async operation is still running. + if (psCmdlet.MyInvocation.BoundParameters.ContainsKey(Debug)) + { + throw new NotSupportedException(Resources.DebugNotSupported); + } + + this.ValidatePolicies(policies); + + this.psCmdlet = psCmdlet; + this.originalThread = Thread.CurrentThread; + } + + /// <summary> + /// Request cancellation for this command. + /// </summary> + public void Cancel() + { + this.source.Cancel(); + } + + /// <summary> + /// Execute the delegate in a MTA thread. + /// Caller must wait on task. + /// </summary> + /// <param name="func">Function to execute.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + internal Task RunOnMTA(Func<Task> func) + { + // .NET 4.8 doesn't support TaskCompletionSource. +#if POWERSHELL_WINDOWS + throw new NotImplementedException(); +#else + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) + { + this.Write(StreamType.Verbose, "Already running on MTA"); + try + { + return func(); + } + finally + { + this.Complete(); + } + } + + this.Write(StreamType.Verbose, "Creating MTA thread"); + var tcs = new TaskCompletionSource(); + var thread = new Thread(() => + { + try + { + func().GetAwaiter().GetResult(); + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + finally + { + this.Complete(); + } + }); + + thread.SetApartmentState(ApartmentState.MTA); + thread.Start(); + return tcs.Task; +#endif + } + + /// <summary> + /// Execute the delegate in a MTA thread. + /// Caller must wait on task. + /// </summary> + /// <param name="func">Function to execute.</param> + /// <typeparam name="TResult">Return type of function.</typeparam> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + internal Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func) + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) + { + this.Write(StreamType.Verbose, "Already running on MTA"); + try + { + return func(); + } + finally + { + this.Complete(); + } + } + + this.Write(StreamType.Verbose, "Creating MTA thread"); + var tcs = new TaskCompletionSource<TResult>(); + var thread = new Thread(() => + { + try + { + var result = func().GetAwaiter().GetResult(); + tcs.SetResult(result); + } + catch (Exception e) + { + tcs.SetException(e); + } + finally + { + this.Complete(); + } + }); + + thread.SetApartmentState(ApartmentState.MTA); + thread.Start(); + return tcs.Task; + } + + /// <summary> + /// Execute the delegate in a MTA thread. + /// Synchronous call. + /// </summary> + /// <param name="func">Function to execute.</param> + /// <typeparam name="TResult">Return type of function.</typeparam> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + internal TResult RunOnMTA<TResult>(Func<TResult> func) + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) + { + this.Write(StreamType.Verbose, "Already running on MTA"); + try + { + return func(); + } + finally + { + this.Complete(); + } + } + + this.Write(StreamType.Verbose, "Creating MTA thread"); + var tcs = new TaskCompletionSource<TResult>(); + var thread = new Thread(() => + { + try + { + var result = func(); + tcs.SetResult(result); + } + catch (Exception e) + { + tcs.SetException(e); + } + finally + { + this.Complete(); + } + }); + + thread.SetApartmentState(ApartmentState.MTA); + thread.Start(); + this.Wait(tcs.Task); + return tcs.Task.Result; + } + + /// <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> + /// <param name="writeCmdlet">The cmdlet that can write to PowerShell.</param> + internal void Wait(Task runningTask, PowerShellCmdlet? writeCmdlet = null) + { + writeCmdlet ??= this; + + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + do + { + this.ConsumeAndWriteStreams(writeCmdlet); + } + while (!(runningTask.IsCompleted && this.queuedStreams.IsCompleted)); + + if (runningTask.IsFaulted) + { + // If IsFaulted is true, the task's Status is equal to Faulted, + // and its Exception property will be non-null. + throw runningTask.Exception!; + } + } + + /// <summary> + /// Writes into the corresponding stream if running on the main thread. + /// Otherwise queue the message. + /// Is the caller responsibility to use the correct types. + /// </summary> + /// <param name="type">Stream type.</param> + /// <param name="data">Data.</param> + internal void Write(StreamType type, object data) + { + if (type == StreamType.Progress) + { + // Keep track of all progress activity. + ProgressRecord progressRecord = (ProgressRecord)data; + if (!this.progressRecords.TryAdd(progressRecord.ActivityId, progressRecord.RecordType)) + { + _ = this.progressRecords.TryUpdate(progressRecord.ActivityId, progressRecord.RecordType, ProgressRecordType.Completed); + } + } + + if (this.originalThread == Thread.CurrentThread) + { + this.CmdletWrite(type, data, this); + return; + } + + this.queuedStreams.Add(new QueuedStream(type, data)); + } + + /// <summary> + /// Helper to compute percentage and write progress for processing activities. + /// </summary> + /// <param name="activityId">Activity id.</param> + /// <param name="activity">The activity in progress.</param> + /// <param name="status">The status of the activity.</param> + /// <param name="completed">Number of completed actions.</param> + /// <param name="total">The expected total.</param> + internal void WriteProgressWithPercentage(int activityId, string activity, string status, int completed, int total) + { + double percentComplete = (double)completed / total; + var record = new ProgressRecord(activityId, activity, status) + { + RecordType = ProgressRecordType.Processing, + PercentComplete = (int)(100.0 * percentComplete), + }; + this.Write(StreamType.Progress, record); + } + + /// <summary> + /// Helper to complete progress records. + /// </summary> + /// <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) + { + var record = new ProgressRecord(activityId, activity, status) + { + RecordType = ProgressRecordType.Completed, + PercentComplete = 100, + }; + this.Write(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. + /// </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) + { + throw new InvalidOperationException(); + } + + // Take from the blocking collection until is completed. + try + { + while (true) + { + var queuedOutput = this.queuedStreams.Take(); + if (queuedOutput != null) + { + this.CmdletWrite(queuedOutput.Type, queuedOutput.Data, writeCmdlet); + } + } + } + catch (InvalidOperationException) + { + // We are done. + // An InvalidOperationException means that Take() was called on a completed collection. + } + } + + /// <summary> + /// Gets a new progress activity id. + /// </summary> + /// <returns>The new progress record id.</returns> + internal int GetNewProgressActivityId() + { + return Interlocked.Increment(ref this.progressActivityId); + } + + /// <summary> + /// Gets the cancellation token. + /// </summary> + /// <returns>CancellationToken.</returns> + internal CancellationToken GetCancellationToken() + { + return this.source.Token; + } + + /// <summary> + /// Gets the current file system location from the cmdlet. + /// </summary> + /// <returns>Path.</returns> + internal string GetCurrentFileSystemLocation() + { + return this.psCmdlet.SessionState.Path.CurrentFileSystemLocation.Path; + } + + /// <summary> + /// Sets a variable. + /// </summary> + /// <param name="variableName">Variable name.</param> + /// <param name="value">Value.</param> + internal void SetVariable(string variableName, object value) + { + this.psCmdlet.SessionState.PSVariable.Set(variableName, value); + } + + /// <summary> + /// Prompts the user if it should continue processing if possible. + /// </summary> + /// <param name="target">Message.</param> + /// <returns>If the operation should continue.</returns> + internal bool ShouldProcess(string target) + { + // If not on the main thread just continue. + if (this.originalThread != Thread.CurrentThread) + { + return true; + } + + return this.psCmdlet.ShouldProcess(target); + } + + private void Complete() + { + this.queuedStreams.CompleteAdding(); + } + + private void CmdletWrite(StreamType streamType, object data, PowerShellCmdlet writeCmdlet) + { + switch (streamType) + { + case StreamType.Debug: + throw new NotSupportedException(); + case StreamType.Verbose: + writeCmdlet.psCmdlet.WriteVerbose((string)data); + break; + case StreamType.Warning: + writeCmdlet.psCmdlet.WriteWarning((string)data); + break; + case StreamType.Error: + writeCmdlet.psCmdlet.WriteError((ErrorRecord)data); + break; + case StreamType.Progress: + // If the activity is already completed don't write progress. + var progressRecord = (ProgressRecord)data; + if (this.progressRecords[progressRecord.ActivityId] == ProgressRecordType.Processing) + { + writeCmdlet.psCmdlet.WriteProgress(progressRecord); + } + + break; + case StreamType.Object: + writeCmdlet.psCmdlet.WriteObject(data); + break; + case StreamType.Information: + writeCmdlet.psCmdlet.WriteInformation(data, WriteInformationTags); + break; + } + } + + private void ValidatePolicies(HashSet<Policy> policies) + { + GroupPolicy groupPolicy = GroupPolicy.GetInstance(); + + if (policies.Contains(Policy.WinGet)) + { + if (!groupPolicy.IsEnabled(Policy.WinGet)) + { + throw new GroupPolicyException(Policy.WinGet, GroupPolicyFailureType.BlockedByPolicy); + } + + policies.Remove(Policy.WinGet); + } + + if (policies.Contains(Policy.Configuration)) + { + if (!groupPolicy.IsEnabled(Policy.Configuration)) + { + throw new GroupPolicyException(Policy.Configuration, GroupPolicyFailureType.BlockedByPolicy); + } + + policies.Remove(Policy.Configuration); + } + + if (policies.Contains(Policy.WinGetCommandLineInterfaces)) + { + if (!groupPolicy.IsEnabled(Policy.WinGetCommandLineInterfaces)) + { + throw new GroupPolicyException(Policy.WinGetCommandLineInterfaces, GroupPolicyFailureType.BlockedByPolicy); + } + + policies.Remove(Policy.WinGetCommandLineInterfaces); + } + + if (policies.Count > 0) + { + throw new NotSupportedException($"Invalid policies {string.Join(",", policies)}"); + } + } + + private class QueuedStream + { + public QueuedStream(StreamType type, object data) + { + this.Type = type; + this.Data = data; + } + + public StreamType Type { get; } + + public object Data { get; } + } + } +} diff --git a/src/PowerShell/CommonFiles/StreamType.cs b/src/PowerShell/CommonFiles/StreamType.cs @@ -0,0 +1,49 @@ +// ----------------------------------------------------------------------------- +// <copyright file="StreamType.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Common.Command +{ + /// <summary> + /// The write stream type of the cmdlet. + /// </summary> + public enum StreamType + { + /// <summary> + /// Debug. + /// </summary> + Debug, + + /// <summary> + /// Verbose. + /// </summary> + Verbose, + + /// <summary> + /// Warning. + /// </summary> + Warning, + + /// <summary> + /// Error. + /// </summary> + Error, + + /// <summary> + /// Progress. + /// </summary> + Progress, + + /// <summary> + /// Object. + /// </summary> + Object, + + /// <summary> + /// Information. + /// </summary> + Information, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs @@ -10,6 +10,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Common; using Microsoft.WinGet.Client.Engine.Helpers; + using Microsoft.WinGet.Common.Command; /// <summary> /// Commands that just calls winget.exe underneath. @@ -55,11 +56,11 @@ namespace Microsoft.WinGet.Client.Engine.Commands if (asPlainText) { - this.PsCmdlet.WriteObject(result.StdOut); + this.Write(StreamType.Object, result.StdOut); } else { - this.PsCmdlet.WriteObject(Utilities.ConvertToHashtable(result.StdOut)); + this.Write(StreamType.Object, Utilities.ConvertToHashtable(result.StdOut)); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/BaseCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/BaseCommand.cs @@ -6,48 +6,23 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common { + using System.Collections.Generic; using System.Management.Automation; - using Microsoft.WinGet.Client.Engine.Common; - using Microsoft.WinGet.Client.Engine.Exceptions; - using Microsoft.WinGet.SharedLib.Exceptions; + using Microsoft.WinGet.Common.Command; using Microsoft.WinGet.SharedLib.PolicySettings; /// <summary> /// Base class for all Cmdlets. /// </summary> - public abstract class BaseCommand + public abstract class BaseCommand : PowerShellCmdlet { /// <summary> /// Initializes a new instance of the <see cref="BaseCommand"/> class. /// </summary> /// <param name="psCmdlet">PSCmdlet.</param> internal BaseCommand(PSCmdlet psCmdlet) - : base() + : base(psCmdlet, new HashSet<Policy> { Policy.WinGet, Policy.WinGetCommandLineInterfaces }) { - // The inproc COM API may deadlock on an STA thread. - if (Utilities.UsesInProcWinget && Utilities.ThreadIsSTA) - { - throw new SingleThreadedApartmentException(); - } - - GroupPolicy groupPolicy = GroupPolicy.GetInstance(); - - if (!groupPolicy.IsEnabled(Policy.WinGet)) - { - throw new GroupPolicyException(Policy.WinGet, GroupPolicyFailureType.BlockedByPolicy); - } - - if (!groupPolicy.IsEnabled(Policy.WinGetCommandLineInterfaces)) - { - throw new GroupPolicyException(Policy.WinGetCommandLineInterfaces, GroupPolicyFailureType.BlockedByPolicy); - } - - this.PsCmdlet = psCmdlet; } - - /// <summary> - /// Gets the caller PSCmdlet. - /// </summary> - protected PSCmdlet PsCmdlet { get; private set; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/FinderCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/FinderCommand.cs @@ -35,31 +35,33 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common /// Gets or sets the field that is matched against the identifier of a package. /// </summary> [Filter(Field = PackageMatchField.Id)] - protected string Id { get; set; } + protected string? Id { get; set; } /// <summary> /// Gets or sets the field that is matched against the name of a package. /// </summary> [Filter(Field = PackageMatchField.Name)] - protected string Name { get; set; } + protected string? Name { get; set; } /// <summary> /// Gets or sets the field that is matched against the moniker of a package. /// </summary> [Filter(Field = PackageMatchField.Moniker)] - protected string Moniker { get; set; } + protected string? Moniker { get; set; } /// <summary> /// Gets or sets the name of the source to search for packages. If null, then all sources are searched. /// </summary> - protected string Source { get; set; } + protected string? Source { get; set; } /// <summary> /// Gets or sets how to match against package fields. /// </summary> - protected string[] Query { get; set; } +#pragma warning disable SA1011 // Closing square brackets should be spaced correctly + protected string[]? Query { get; set; } +#pragma warning restore SA1011 // Closing square brackets should be spaced correctly - private string QueryAsJoinedString + private string? QueryAsJoinedString { get { @@ -98,7 +100,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common protected virtual void SetQueryInFindPackagesOptions( ref FindPackagesOptions options, string match, - string value) + string? value) { var selector = ManagementDeploymentFactory.Instance.CreatePackageMatchFilter(); selector.Field = PackageMatchField.CatalogDefault; @@ -111,7 +113,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common ref FindPackagesOptions options, PackageMatchField field, PackageFieldMatchOption match, - string value) + string? value) { if (value != null) { @@ -187,7 +189,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common if (info.GetCustomAttribute(typeof(FilterAttribute), true) is FilterAttribute attribute) { PackageMatchField field = attribute.Field; - string value = info.GetValue(this, null) as string; + string? value = info.GetValue(this, null) as string; this.AddFilterToFindPackagesOptionsIfNotNull(ref options, field, match, value); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/FinderExtendedCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/FinderExtendedCommand.cs @@ -31,13 +31,13 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common /// Gets or sets the filter that is matched against the tags of the package. /// </summary> [Filter(Field = PackageMatchField.Tag)] - protected string Tag { get; set; } + protected string? Tag { get; set; } /// <summary> /// Gets or sets the filter that is matched against the commands of the package. /// </summary> [Filter(Field = PackageMatchField.Command)] - protected string Command { get; set; } + protected string? Command { get; set; } /// <summary> /// Gets or sets the maximum number of results returned. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/InstallCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/InstallCommand.cs @@ -29,17 +29,17 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common /// <summary> /// Gets or sets the override arguments to be passed on to the installer. /// </summary> - protected string Override { get; set; } + protected string? Override { get; set; } /// <summary> /// Gets or sets the arguments to be passed on to the installer in addition to the defaults. /// </summary> - protected string Custom { get; set; } + protected string? Custom { get; set; } /// <summary> /// Gets or sets the installation location. /// </summary> - protected string Location { get; set; } + protected string? Location { get; set; } /// <summary> /// Gets or sets a value indicating whether to skip the installer hash validation check. @@ -54,7 +54,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common /// <summary> /// Gets or sets the optional HTTP Header to pass on to the REST Source. /// </summary> - protected string Header { get; set; } + protected string? Header { get; set; } /// <summary> /// Gets the install options from the configured parameters. @@ -65,7 +65,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common /// <param name="version">The <see cref="PackageVersionId" /> to install.</param> /// <param name="mode">Package install mode as string.</param> /// <returns>An <see cref="InstallOptions" /> instance.</returns> - protected virtual InstallOptions GetInstallOptions(PackageVersionId version, string mode) + protected virtual InstallOptions GetInstallOptions(PackageVersionId? version, string mode) { InstallOptions options = ManagementDeploymentFactory.Instance.CreateInstallOptions(); options.AllowHashMismatch = this.AllowHashMismatch; @@ -115,10 +115,11 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common IAsyncOperationWithProgress<InstallResult, InstallProgress> operation, string activity) { - WriteProgressAdapter adapter = new (this.PsCmdlet); + var activityId = this.GetNewProgressActivityId(); + WriteProgressAdapter adapter = new (this); operation.Progress = (context, progress) => { - ProgressRecord record = new (1, activity, progress.State.ToString()) + ProgressRecord record = new (activityId, activity, progress.State.ToString()) { RecordType = ProgressRecordType.Processing, }; @@ -137,7 +138,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common }; operation.Completed = (context, status) => { - adapter.WriteProgress(new ProgressRecord(1, activity, status.ToString()) + adapter.WriteProgress(new ProgressRecord(activityId, activity, status.ToString()) { RecordType = ProgressRecordType.Completed, }); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/ManagementDeploymentCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/ManagementDeploymentCommand.cs @@ -11,9 +11,10 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common using System.Management.Automation; using System.Runtime.InteropServices; 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.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// This is the base class for all of the commands in this module that use the COM APIs. @@ -40,12 +41,29 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common } /// <summary> + /// Executes the cmdlet. All cmdlets that uses the COM APIs 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> + /// <param name="func">Cmdlet function.</param> + /// <returns>The result of the cmdlet.</returns> + protected TResult Execute<TResult>(Func<TResult> func) + { + if (Utilities.UsesInProcWinget) + { + return this.RunOnMTA(func); + } + + return func(); + } + + /// <summary> /// Retrieves the specified source or all sources if <paramref name="source" /> is null. /// </summary> /// <returns>A list of <see cref="PackageCatalogReference" /> instances.</returns> /// <param name="source">The name of the source to retrieve. If null, then all sources are returned.</param> /// <exception cref="ArgumentException">The source does not exist.</exception> - protected IReadOnlyList<PackageCatalogReference> GetPackageCatalogReferences(string source) + protected IReadOnlyList<PackageCatalogReference> GetPackageCatalogReferences(string? source) { if (string.IsNullOrEmpty(source)) { @@ -55,8 +73,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common { return new List<PackageCatalogReference>() { - PackageManagerWrapper.Instance.GetPackageCatalogByName(source) - ?? throw new InvalidSourceException(source), + PackageManagerWrapper.Instance.GetPackageCatalogByName(source!) + ?? throw new InvalidSourceException(source!), }; } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/PackageCommand.cs @@ -36,35 +36,40 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common /// <remarks> /// Must match the name of the <see cref="CatalogPackage" /> field on the <see cref="MatchResult" /> class. /// </remarks> - protected PSCatalogPackage CatalogPackage { get; set; } = null; + protected PSCatalogPackage? CatalogPackage { get; set; } = null; /// <summary> /// Gets or sets the version to install. /// </summary> - protected string Version { get; set; } + protected string? Version { get; set; } /// <summary> /// Gets or sets the path to the logging file. /// </summary> - protected string Log { get; set; } + protected string? Log { get; set; } /// <summary> /// Executes a command targeting a specific package version. /// </summary> + /// <typeparam name="TResult">Type of callback's result.</typeparam> /// <param name="behavior">The <see cref="CompositeSearchBehavior" /> value.</param> /// <param name="match">The match option.</param> /// <param name="callback">The method to call after retrieving the package and version to operate upon.</param> - protected void GetPackageAndExecute( + /// <returns>Result of the callback.</returns> + protected TResult? GetPackageAndExecute<TResult>( CompositeSearchBehavior behavior, PackageFieldMatchOption match, - Action<CatalogPackage, PackageVersionId> callback) + Func<CatalogPackage, PackageVersionId?, TResult> callback) + where TResult : class { CatalogPackage package = this.GetCatalogPackage(behavior, match); - PackageVersionId version = this.GetPackageVersionId(package); - if (this.PsCmdlet.ShouldProcess(package.ToString(version))) + PackageVersionId? version = this.GetPackageVersionId(package); + if (this.ShouldProcess(package.ToString(version))) { - callback(package, version); + return callback(package, version); } + + return null; } /// <summary> @@ -79,7 +84,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common protected override void SetQueryInFindPackagesOptions( ref FindPackagesOptions options, string match, - string value) + string? value) { var matchOption = PSEnumHelpers.ToPackageFieldMatchOption(match); foreach (PackageMatchField field in new PackageMatchField[] { PackageMatchField.Id, PackageMatchField.Name, PackageMatchField.Moniker }) @@ -120,7 +125,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common } } - private PackageVersionId GetPackageVersionId(CatalogPackage package) + private PackageVersionId? GetPackageVersionId(CatalogPackage package) { if (this.Version != null) { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs @@ -11,6 +11,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Helpers; using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Common.Command; /// <summary> /// Searches configured sources for packages. @@ -60,10 +61,14 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="psPackageFieldMatchOption">PSPackageFieldMatchOption.</param> public void Find(string psPackageFieldMatchOption) { - var results = this.FindPackages(CompositeSearchBehavior.RemotePackagesFromRemoteCatalogs, PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption)); + var results = this.Execute( + () => this.FindPackages( + CompositeSearchBehavior.RemotePackagesFromRemoteCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption))); + for (var i = 0; i < results.Count; i++) { - this.PsCmdlet.WriteObject(new PSFoundCatalogPackage(results[i].CatalogPackage)); + this.Write(StreamType.Object, new PSFoundCatalogPackage(results[i].CatalogPackage)); } } @@ -73,10 +78,13 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="psPackageFieldMatchOption">PSPackageFieldMatchOption.</param> public void Get(string psPackageFieldMatchOption) { - var results = this.FindPackages(CompositeSearchBehavior.LocalCatalogs, PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption)); + var results = this.Execute( + () => this.FindPackages( + CompositeSearchBehavior.LocalCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption))); for (var i = 0; i < results.Count; i++) { - this.PsCmdlet.WriteObject(new PSInstalledCatalogPackage(results[i].CatalogPackage)); + this.Write(StreamType.Object, new PSInstalledCatalogPackage(results[i].CatalogPackage)); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs @@ -10,8 +10,9 @@ namespace Microsoft.WinGet.Client.Engine.Commands using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Helpers; - using Microsoft.WinGet.Client.Engine.Properties; using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; /// <summary> /// Installs or updates a package from the pipeline or from a configured source. @@ -94,23 +95,28 @@ namespace Microsoft.WinGet.Client.Engine.Commands string psPackageFieldMatchOption, string psPackageInstallMode) { - this.GetPackageAndExecute( - CompositeSearchBehavior.RemotePackagesFromRemoteCatalogs, - PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), - (package, version) => - { - InstallOptions options = this.GetInstallOptions(version, psPackageInstallMode); - if (psProcessorArchitecture != "Default") + var result = this.Execute( + () => this.GetPackageAndExecute( + CompositeSearchBehavior.RemotePackagesFromRemoteCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), + (package, version) => { - options.AllowedArchitectures.Clear(); - options.AllowedArchitectures.Add(PSEnumHelpers.ToProcessorArchitecture(psProcessorArchitecture)); - } + InstallOptions options = this.GetInstallOptions(version, psPackageInstallMode); + if (psProcessorArchitecture != "Default") + { + options.AllowedArchitectures.Clear(); + options.AllowedArchitectures.Add(PSEnumHelpers.ToProcessorArchitecture(psProcessorArchitecture)); + } - options.PackageInstallScope = PSEnumHelpers.ToPackageInstallScope(psPackageInstallScope); + options.PackageInstallScope = PSEnumHelpers.ToPackageInstallScope(psPackageInstallScope); - InstallResult result = this.InstallPackage(package, options); - this.PsCmdlet.WriteObject(new PSInstallResult(result)); - }); + return this.InstallPackage(package, options); + })); + + if (result != null) + { + this.Write(StreamType.Object, new PSInstallResult(result)); + } } /// <summary> @@ -124,17 +130,21 @@ namespace Microsoft.WinGet.Client.Engine.Commands string psPackageFieldMatchOption, string psPackageInstallMode) { - this.GetPackageAndExecute( - CompositeSearchBehavior.LocalCatalogs, - PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), - (package, version) => - { - InstallOptions options = this.GetInstallOptions(version, psPackageInstallMode); - options.AllowUpgradeToUnknownVersion = includeUnknown; + var result = this.Execute( + () => this.GetPackageAndExecute( + CompositeSearchBehavior.LocalCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), + (package, version) => + { + InstallOptions options = this.GetInstallOptions(version, psPackageInstallMode); + options.AllowUpgradeToUnknownVersion = includeUnknown; + return this.UpgradePackage(package, options); + })); - InstallResult result = this.UpgradePackage(package, options); - this.PsCmdlet.WriteObject(new PSInstallResult(result)); - }); + if (result != null) + { + this.Write(StreamType.Object, new PSInstallResult(result)); + } } private InstallResult InstallPackage( diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/SourceCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/SourceCommand.cs @@ -9,6 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Common.Command; /// <summary> /// Wrapper for source cmdlets. @@ -31,10 +32,11 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="name">Optional name.</param> public void Get(string name) { - var results = this.GetPackageCatalogReferences(name); + var results = this.Execute( + () => this.GetPackageCatalogReferences(name)); for (var i = 0; i < results.Count; i++) { - this.PsCmdlet.WriteObject(new PSSourceResult(results[i])); + this.Write(StreamType.Object, new PSSourceResult(results[i])); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UninstallPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UninstallPackageCommand.cs @@ -11,8 +11,9 @@ namespace Microsoft.WinGet.Client.Engine.Commands using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Helpers; - using Microsoft.WinGet.Client.Engine.Properties; using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; /// <summary> /// Uninstalls a package from the local system. @@ -71,19 +72,24 @@ namespace Microsoft.WinGet.Client.Engine.Commands string psPackageFieldMatchOption, bool force) { - this.GetPackageAndExecute( - CompositeSearchBehavior.LocalCatalogs, - PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), - (package, version) => - { - UninstallOptions options = this.GetUninstallOptions(version, PSEnumHelpers.ToPackageUninstallMode(psPackageUninstallMode), force); - UninstallResult result = this.UninstallPackage(package, options); - this.PsCmdlet.WriteObject(new PSUninstallResult(result)); - }); + var result = this.Execute( + () => this.GetPackageAndExecute( + CompositeSearchBehavior.LocalCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), + (package, version) => + { + UninstallOptions options = this.GetUninstallOptions(version, PSEnumHelpers.ToPackageUninstallMode(psPackageUninstallMode), force); + return this.UninstallPackage(package, options); + })); + + if (result != null) + { + this.Write(StreamType.Object, new PSUninstallResult(result)); + } } private UninstallOptions GetUninstallOptions( - PackageVersionId version, + PackageVersionId? version, PackageUninstallMode packageUninstallMode, bool force) { @@ -113,17 +119,19 @@ namespace Microsoft.WinGet.Client.Engine.Commands package.Name); var operation = PackageManagerWrapper.Instance.UninstallPackageAsync(package, options); - WriteProgressAdapter adapter = new (this.PsCmdlet); + + var activityId = this.GetNewProgressActivityId(); + WriteProgressAdapter adapter = new (this); operation.Progress = (context, progress) => { - adapter.WriteProgress(new ProgressRecord(1, activity, progress.State.ToString()) + adapter.WriteProgress(new ProgressRecord(activityId, activity, progress.State.ToString()) { RecordType = ProgressRecordType.Processing, }); }; operation.Completed = (context, status) => { - adapter.WriteProgress(new ProgressRecord(1, activity, status.ToString()) + adapter.WriteProgress(new ProgressRecord(activityId, activity, status.ToString()) { RecordType = ProgressRecordType.Completed, }); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UserSettingsCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UserSettingsCommand.cs @@ -16,6 +16,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands using Microsoft.WinGet.Client.Engine.Common; using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Helpers; + using Microsoft.WinGet.Common.Command; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -27,16 +28,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands private const string SchemaKey = "$schema"; private const string SchemaValue = "https://aka.ms/winget-settings.schema.json"; - private static readonly string WinGetSettingsFilePath; - - static UserSettingsCommand() - { - var wingetCliWrapper = new WingetCLIWrapper(); - var settingsResult = wingetCliWrapper.RunCommand("settings", "export"); - - // Read the user settings file property. - WinGetSettingsFilePath = (string)Utilities.ConvertToHashtable(settingsResult.StdOut)["userSettingsFile"]; - } + private static string? winGetSettingsFilePath; /// <summary> /// Initializes a new instance of the <see cref="UserSettingsCommand"/> class. @@ -45,6 +37,18 @@ namespace Microsoft.WinGet.Client.Engine.Commands public UserSettingsCommand(PSCmdlet psCmdlet) : base(psCmdlet) { + // Doing it in the static constructor will show the user running in system context: + // The type initializer for 'Microsoft.WinGet.Client.Engine.Commands.UserSettingsCommand' threw an exception. + // Here would be "The specified method is not supported." + if (winGetSettingsFilePath == null) + { + var wingetCliWrapper = new WingetCLIWrapper(); + var settingsResult = wingetCliWrapper.RunCommand("settings", "export"); + + // Read the user settings file property. + var userSettingsFile = Utilities.ConvertToHashtable(settingsResult.StdOut)["userSettingsFile"] ?? throw new ArgumentNullException("userSettingsFile"); + winGetSettingsFilePath = (string)userSettingsFile; + } } /// <summary> @@ -52,7 +56,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// </summary> public void Get() { - this.PsCmdlet.WriteObject(this.GetLocalSettingsAsHashtable()); + this.Write(StreamType.Object, this.GetLocalSettingsAsHashtable()); } /// <summary> @@ -62,7 +66,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="ignoreNotSet">Ignore comparing settings that are not part of the input.</param> public void Test(Hashtable userSettings, bool ignoreNotSet) { - this.PsCmdlet.WriteObject(this.CompareUserSettings(userSettings, ignoreNotSet)); + this.Write(StreamType.Object, this.CompareUserSettings(userSettings, ignoreNotSet)); } /// <summary> @@ -100,10 +104,10 @@ namespace Microsoft.WinGet.Client.Engine.Commands // Write settings. var settingsJson = orderedSettings.ToString(Formatting.Indented); File.WriteAllText( - WinGetSettingsFilePath, + winGetSettingsFilePath!, settingsJson); - this.PsCmdlet.WriteObject(Utilities.ConvertToHashtable(settingsJson)); + this.Write(StreamType.Object, Utilities.ConvertToHashtable(settingsJson)); } private static JObject HashtableToJObject(Hashtable hashtable) @@ -113,8 +117,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands private Hashtable GetLocalSettingsAsHashtable() { - var content = File.Exists(WinGetSettingsFilePath) ? - File.ReadAllText(WinGetSettingsFilePath) : + var content = File.Exists(winGetSettingsFilePath) ? + File.ReadAllText(winGetSettingsFilePath) : string.Empty; return Utilities.ConvertToHashtable(content); @@ -124,13 +128,13 @@ namespace Microsoft.WinGet.Client.Engine.Commands { try { - return File.Exists(WinGetSettingsFilePath) ? - JObject.Parse(File.ReadAllText(WinGetSettingsFilePath)) : + return File.Exists(winGetSettingsFilePath) ? + JObject.Parse(File.ReadAllText(winGetSettingsFilePath)) : new JObject(); } catch (JsonReaderException e) { - this.PsCmdlet.WriteDebug(e.Message); + this.Write(StreamType.Verbose, e.Message); throw new UserSettingsReadException(e); } } @@ -162,7 +166,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands } catch (Exception e) { - this.PsCmdlet.WriteDebug(e.Message); + this.Write(StreamType.Verbose, e.Message); return false; } } @@ -175,27 +179,34 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="json">Main json.</param> /// <param name="otherJson">otherJson.</param> /// <returns>True is otherJson partially contains json.</returns> - private bool PartialDeepEquals(JToken json, JToken otherJson) + private bool PartialDeepEquals(JToken json, JToken? otherJson) { if (JToken.DeepEquals(json, otherJson)) { return true; } + if (otherJson == null) + { + return false; + } + // If they are a JValue (string, integer, date, etc) or they are a JArray and DeepEquals fails then not equal. if ((json is JValue && otherJson is JValue) || (json is JArray && otherJson is JArray)) { - this.PsCmdlet.WriteDebug($"'{json.ToString(Newtonsoft.Json.Formatting.None)}' != " + - $"'{otherJson.ToString(Newtonsoft.Json.Formatting.None)}'"); + this.Write( + StreamType.Verbose, + $"'{json.ToString(Formatting.None)}' != '{otherJson.ToString(Formatting.None)}'"); return false; } // If its not the same type then don't bother. if (json.Type != otherJson.Type) { - this.PsCmdlet.WriteDebug($"Mismatch types '{json.ToString(Newtonsoft.Json.Formatting.None)}' " + - $"'{otherJson.ToString(Newtonsoft.Json.Formatting.None)}'"); + this.Write( + StreamType.Verbose, + $"Mismatch types '{json.ToString(Formatting.None)}' '{otherJson.ToString(Formatting.None)}'"); return false; } @@ -211,7 +222,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands // If the property is not there then give up. if (!otherJObject.ContainsKey(property.Name)) { - this.PsCmdlet.WriteDebug($"{property.Name} not found."); + this.Write(StreamType.Verbose, $"{property.Name} not found."); return false; } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/VersionCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/VersionCommand.cs @@ -9,6 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Helpers; + using Microsoft.WinGet.Common.Command; /// <summary> /// Version commands. @@ -29,7 +30,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// </summary> public void Get() { - this.PsCmdlet.WriteObject(WinGetVersion.InstalledWinGetVersion.TagVersion); + this.Write(StreamType.Object, WinGetVersion.InstalledWinGetVersion.TagVersion); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -13,7 +13,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands using Microsoft.WinGet.Client.Engine.Common; using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Helpers; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; using static Microsoft.WinGet.Client.Engine.Common.Constants; /// <summary> @@ -49,7 +50,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="expectedVersion">The expected version.</param> public void Assert(string expectedVersion) { - WinGetIntegrity.AssertWinGet(this.PsCmdlet, expectedVersion); + WinGetIntegrity.AssertWinGet(this, expectedVersion); } /// <summary> @@ -96,8 +97,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands { try { - WinGetIntegrity.AssertWinGet(this.PsCmdlet, expectedVersion); - this.PsCmdlet.WriteDebug($"WinGet is in a good state."); + WinGetIntegrity.AssertWinGet(this, expectedVersion); + this.Write(StreamType.Verbose, $"WinGet is in a good state."); currentCategory = IntegrityCategory.Installed; } catch (WinGetIntegrityException e) @@ -106,11 +107,11 @@ namespace Microsoft.WinGet.Client.Engine.Commands if (seenCategories.Contains(currentCategory)) { - this.PsCmdlet.WriteDebug($"{currentCategory} encountered previously"); + this.Write(StreamType.Verbose, $"{currentCategory} encountered previously"); throw; } - this.PsCmdlet.WriteDebug($"Integrity category type: {currentCategory}"); + this.Write(StreamType.Verbose, $"Integrity category type: {currentCategory}"); seenCategories.Add(currentCategory); switch (currentCategory) @@ -156,10 +157,13 @@ namespace Microsoft.WinGet.Client.Engine.Commands var installedVersion = WinGetVersion.InstalledWinGetVersion; bool isDowngrade = installedVersion.CompareAsDeployment(toInstallVersion) > 0; - this.PsCmdlet.WriteDebug($"Installed WinGet version '{installedVersion.TagVersion}' " + + string message = $"Installed WinGet version '{installedVersion.TagVersion}' " + $"Installing WinGet version '{toInstallVersion.TagVersion}' " + - $"Is downgrade {isDowngrade}"); - var appxModule = new AppxModuleHelper(this.PsCmdlet); + $"Is downgrade {isDowngrade}"; + this.Write( + StreamType.Verbose, + message); + var appxModule = new AppxModuleHelper(this); appxModule.InstallFromGitHubRelease(toInstallVersion.TagVersion, allUsers, isDowngrade); } @@ -174,13 +178,13 @@ namespace Microsoft.WinGet.Client.Engine.Commands toInstallVersion = gitHubClient.GetLatestVersionTagName(false); } - var appxModule = new AppxModuleHelper(this.PsCmdlet); + var appxModule = new AppxModuleHelper(this); appxModule.InstallFromGitHubRelease(toInstallVersion, allUsers, false); } private void Register() { - var appxModule = new AppxModuleHelper(this.PsCmdlet); + var appxModule = new AppxModuleHelper(this); appxModule.RegisterAppInstaller(); } @@ -190,12 +194,12 @@ namespace Microsoft.WinGet.Client.Engine.Commands Utilities.AddWindowsAppToPath(); // Update this sessions PowerShell environment so the user doesn't have to restart the terminal. - string envPathUser = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.User); - string envPathMachine = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.Machine); + string? envPathUser = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.User); + string? envPathMachine = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.Machine); string newPwshPathEnv = $"{envPathMachine};{envPathUser}"; - this.PsCmdlet.SessionState.PSVariable.Set(EnvPath, newPwshPathEnv); + this.SetVariable(EnvPath, newPwshPathEnv); - this.PsCmdlet.WriteDebug($"PATH environment variable updated"); + this.Write(StreamType.Verbose, $"PATH environment variable updated"); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Utilities.cs @@ -13,7 +13,7 @@ namespace Microsoft.WinGet.Client.Engine.Common using System.Management.Automation; using System.Security.Principal; using System.Threading; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -110,7 +110,7 @@ namespace Microsoft.WinGet.Client.Engine.Common public static void AddWindowsAppToPath() { var scope = EnvironmentVariableTarget.User; - string envPathValue = Environment.GetEnvironmentVariable(Constants.PathEnvVar, scope); + string? envPathValue = Environment.GetEnvironmentVariable(Constants.PathEnvVar, scope); if (string.IsNullOrEmpty(envPathValue) || !envPathValue.Contains(Utilities.LocalDataWindowsAppPath)) { Environment.SetEnvironmentVariable( @@ -185,9 +185,9 @@ namespace Microsoft.WinGet.Client.Engine.Common return result; } - private static ICollection<object> PopulateHashTableFromJArray(JArray list) + private static ICollection<object?> PopulateHashTableFromJArray(JArray list) { - var result = new object[list.Count]; + var result = new object?[list.Count]; for (var index = 0; index < list.Count; index++) { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -12,7 +12,8 @@ namespace Microsoft.WinGet.Client.Engine.Common using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Helpers; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; /// <summary> /// Validates winget runs correctly. @@ -22,9 +23,9 @@ namespace Microsoft.WinGet.Client.Engine.Common /// <summary> /// Verifies winget runs correctly. If it doesn't, tries to find the reason why it failed. /// </summary> - /// <param name="psCmdlet">The calling cmdlet.</param> + /// <param name="pwshCmdlet">The calling cmdlet.</param> /// <param name="expectedVersion">Expected version.</param> - public static void AssertWinGet(PSCmdlet psCmdlet, string expectedVersion) + public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVersion) { // In-proc shouldn't have other dependencies and thus should be ok. if (Utilities.UsesInProcWinget) @@ -48,17 +49,17 @@ namespace Microsoft.WinGet.Client.Engine.Common } catch (Win32Exception e) { - psCmdlet.WriteDebug($"'winget.exe' Win32Exception {e.Message}"); - throw new WinGetIntegrityException(GetReason(psCmdlet)); + pwshCmdlet.Write(StreamType.Verbose, $"'winget.exe' Win32Exception {e.Message}"); + throw new WinGetIntegrityException(GetReason(pwshCmdlet)); } catch (Exception e) when (e is WinGetCLIException || e is WinGetCLITimeoutException) { - psCmdlet.WriteDebug($"'winget.exe' WinGetCLIException {e.Message}"); + pwshCmdlet.Write(StreamType.Verbose, $"'winget.exe' WinGetCLIException {e.Message}"); throw new WinGetIntegrityException(IntegrityCategory.Failure, e); } catch (Exception e) { - psCmdlet.WriteDebug($"'winget.exe' Exception {e.Message}"); + pwshCmdlet.Write(StreamType.Verbose, $"'winget.exe' Exception {e.Message}"); throw new WinGetIntegrityException(IntegrityCategory.Unknown, e); } @@ -80,7 +81,7 @@ namespace Microsoft.WinGet.Client.Engine.Common } } - private static IntegrityCategory GetReason(PSCmdlet psCmdlet) + private static IntegrityCategory GetReason(PowerShellCmdlet pwshCmdlet) { // Ok, so you are here because calling winget --version failed. Lets try to figure out why. @@ -96,7 +97,7 @@ namespace Microsoft.WinGet.Client.Engine.Common } catch (ApplicationFailedException e) { - psCmdlet.WriteDebug(e.Message); + pwshCmdlet.Write(StreamType.Verbose, e.Message); return IntegrityCategory.AppInstallerNoLicense; } catch (Exception) @@ -112,7 +113,7 @@ namespace Microsoft.WinGet.Client.Engine.Common if (File.Exists(wingetAliasPath)) { // App execution alias is enabled. Then maybe the path? - string envPath = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.User); + string? envPath = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.User); if (string.IsNullOrEmpty(envPath) || !envPath.EndsWith(Utilities.LocalDataWindowsAppPath) || !envPath.Contains($"{Utilities.LocalDataWindowsAppPath};")) @@ -136,8 +137,8 @@ namespace Microsoft.WinGet.Client.Engine.Common // It could be that AppInstaller package is old or the package is not // registered at this point. To know that, call Get-AppxPackage. - var appxModule = new AppxModuleHelper(psCmdlet); - string version = appxModule.GetAppInstallerPropertyValue("Version"); + var appxModule = new AppxModuleHelper(pwshCmdlet); + string? version = appxModule.GetAppInstallerPropertyValue("Version"); if (version is null) { // This can happen in Windows Sandbox. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/CatalogConnectException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/CatalogConnectException.cs @@ -8,7 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System; using System.Management.Automation; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// Failed connecting to catalog. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/FindPackagesException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/FindPackagesException.cs @@ -9,7 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions using System; using System.Management.Automation; using Microsoft.Management.Deployment; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// Raised when there is an error searching for packages. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/InvalidSourceException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/InvalidSourceException.cs @@ -7,7 +7,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// Invalid source. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/InvalidVersionException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/InvalidVersionException.cs @@ -7,7 +7,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// Invalid version. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/NoPackageFoundException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/NoPackageFoundException.cs @@ -8,7 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System; using System.Management.Automation; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// No package found. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/SingleThreadedApartmentException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/SingleThreadedApartmentException.cs @@ -8,7 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System; using System.Management.Automation; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// No package found. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/UserSettingsReadException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/UserSettingsReadException.cs @@ -8,7 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System; using System.Management.Automation; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// Settings.json file is invalid. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/VagueCriteriaException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/VagueCriteriaException.cs @@ -11,7 +11,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions using System.Management.Automation; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Extensions; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// Raised when search criteria for installing or updating a package is too vague. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetCLIException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetCLIException.cs @@ -7,7 +7,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System.Management.Automation; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// WinGet cli exception. @@ -22,7 +22,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions /// <param name="exitCode">Exit code.</param> /// <param name="stdOut">Standard output.</param> /// <param name="stdErr">Standard error.</param> - public WinGetCLIException(string command, string parameters, int exitCode, string stdOut, string stdErr) + public WinGetCLIException(string command, string? parameters, int exitCode, string stdOut, string stdErr) : base(string.Format(Resources.WinGetCLIExceptionMessage, command, parameters, exitCode)) { this.Command = command; @@ -40,7 +40,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions /// <summary> /// Gets the parameters. /// </summary> - public string Parameters { get; private set; } + public string? Parameters { get; private set; } /// <summary> /// Gets the exit code. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetCLITimeoutException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetCLITimeoutException.cs @@ -7,7 +7,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// Time out exception for a winget cli command. @@ -19,7 +19,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions /// </summary> /// <param name="command">Command.</param> /// <param name="parameters">Parameters.</param> - public WinGetCLITimeoutException(string command, string parameters) + public WinGetCLITimeoutException(string command, string? parameters) : base(string.Format(Resources.WinGetCLITimeoutExceptionMessage, command, parameters)) { } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs @@ -9,7 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions using System; using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Common; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// WinGet Integrity exception. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetRepairException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetRepairException.cs @@ -9,7 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions using System; using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Common; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// WinGet repair exception. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WindowsPowerShellNotSupported.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WindowsPowerShellNotSupported.cs @@ -8,7 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { using System; using System.Management.Automation; - using Microsoft.WinGet.Client.Engine.Properties; + using Microsoft.WinGet.Resources; /// <summary> /// Windows PowerShell is not supported. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Extensions/CatalogPackageExtensions.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Extensions/CatalogPackageExtensions.cs @@ -21,7 +21,7 @@ namespace Microsoft.WinGet.Client.Engine.Extensions /// <returns>A <see cref="string" /> instance.</returns> public static string ToString( this CatalogPackage package, - PackageVersionId version) + PackageVersionId? version) { if ((version != null) || (package.AvailableVersions.Count > 0)) { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs @@ -13,6 +13,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers using System.Management.Automation; using System.Runtime.InteropServices; using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Common.Command; using static Microsoft.WinGet.Client.Engine.Common.Constants; /// <summary> @@ -71,22 +72,22 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private const string XamlAssetArm = "Microsoft.UI.Xaml.2.7.arm.appx"; private const string XamlAssetArm64 = "Microsoft.UI.Xaml.2.7.arm64.appx"; - private readonly PSCmdlet psCmdlet; + private readonly PowerShellCmdlet pwshCmdlet; /// <summary> /// Initializes a new instance of the <see cref="AppxModuleHelper"/> class. /// </summary> - /// <param name="psCmdlet">The calling cmdlet.</param> - public AppxModuleHelper(PSCmdlet psCmdlet) + /// <param name="pwshCmdlet">The calling cmdlet.</param> + public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) { - this.psCmdlet = psCmdlet; + this.pwshCmdlet = pwshCmdlet; } /// <summary> /// Calls Get-AppxPackage Microsoft.DesktopAppInstaller. /// </summary> /// <returns>Result of Get-AppxPackage.</returns> - public PSObject GetAppInstallerObject() + public PSObject? GetAppInstallerObject() { return this.GetAppxObject(AppInstallerName); } @@ -96,9 +97,9 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// </summary> /// <param name="propertyName">Property name.</param> /// <returns>Value, null if doesn't exist.</returns> - public string GetAppInstallerPropertyValue(string propertyName) + public string? GetAppInstallerPropertyValue(string propertyName) { - string result = null; + string? result = null; var packageObj = this.GetAppInstallerObject(); if (packageObj is not null) { @@ -117,7 +118,13 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// </summary> public void RegisterAppInstaller() { - string packageFullName = this.GetAppInstallerPropertyValue(PackageFullName); + string? packageFullName = this.GetAppInstallerPropertyValue(PackageFullName); + + if (packageFullName == null) + { + throw new ArgumentNullException(PackageFullName); + } + string appxManifestPath = System.IO.Path.Combine( Utilities.ProgramFilesWindowsAppPath, packageFullName, @@ -194,7 +201,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } catch (RuntimeException e) { - this.psCmdlet.WriteDebug($"Failed installing bundle via Add-AppxProvisionedPackage {e}"); + this.pwshCmdlet.Write(StreamType.Verbose, $"Failed installing bundle via Add-AppxProvisionedPackage {e}"); throw e; } } @@ -218,12 +225,12 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } catch (RuntimeException e) { - this.psCmdlet.WriteDebug($"Failed installing bundle via Add-AppxPackage {e}"); + this.pwshCmdlet.Write(StreamType.Verbose, $"Failed installing bundle via Add-AppxPackage {e}"); throw e; } } - private PSObject GetAppxObject(string packageName) + private PSObject? GetAppxObject(string packageName) { return this.ExecuteAppxCmdlet( GetAppxPackage, @@ -266,7 +273,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { foreach (dynamic psobject in result) { - string versionString = psobject?.Version?.ToString(); + string? versionString = psobject?.Version?.ToString(); if (versionString == null) { continue; @@ -276,7 +283,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers if (packageVersion >= minimumVersion) { - this.psCmdlet.WriteDebug($"VCLibs dependency satisfied by: {psobject?.PackageFullName ?? "<null>"}"); + this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs dependency satisfied by: {psobject?.PackageFullName ?? "<null>"}"); isInstalled = true; break; } @@ -285,7 +292,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers if (!isInstalled) { - this.psCmdlet.WriteDebug("Couldn't find required VCLibs package"); + this.pwshCmdlet.Write(StreamType.Verbose, "Couldn't find required VCLibs package"); var vcLibsDependencies = new List<string>(); var arch = RuntimeInformation.OSArchitecture; @@ -317,7 +324,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } else { - this.psCmdlet.WriteDebug($"VCLibs are updated."); + this.pwshCmdlet.Write(StreamType.Verbose, $"VCLibs are updated."); } } @@ -360,7 +367,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } } - private void AddAppxPackageAsUri(string packageUri, IList<string> options = null) + private void AddAppxPackageAsUri(string packageUri, IList<string>? options = null) { try { @@ -378,18 +385,18 @@ namespace Microsoft.WinGet.Client.Engine.Helpers // 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.pwshCmdlet.Write(StreamType.Verbose, $"Failed adding package [{packageUri}]. Retrying downloading it."); this.DownloadPackageAndAdd(packageUri, options); } else { - this.psCmdlet.WriteError(e.ErrorRecord); + this.pwshCmdlet.Write(StreamType.Error, e.ErrorRecord); throw e; } } } - private void DownloadPackageAndAdd(string packageUrl, IList<string> options) + private void DownloadPackageAndAdd(string packageUrl, IList<string>? options) { using var tempFile = new TempFile(); @@ -407,7 +414,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers options); } - private Collection<PSObject> ExecuteAppxCmdlet(string cmdlet, Dictionary<string, object> parameters = null, IList<string> options = null) + private Collection<PSObject> ExecuteAppxCmdlet(string cmdlet, Dictionary<string, object>? parameters = null, IList<string>? options = null) { var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); @@ -447,7 +454,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } } - this.psCmdlet.WriteDebug($"Executing Appx cmdlet {cmd}"); + 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/ManagementDeploymentFactory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs @@ -43,19 +43,19 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private static readonly Guid DownloadOptionsClsid = Guid.Parse("8EF324ED-367C-4880-83E5-BB2ABD0B72F6"); #endif [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static readonly Type PackageManagerType = Type.GetTypeFromCLSID(PackageManagerClsid); + private static readonly Type? PackageManagerType = Type.GetTypeFromCLSID(PackageManagerClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static readonly Type FindPackagesOptionsType = Type.GetTypeFromCLSID(FindPackagesOptionsClsid); + private static readonly Type? FindPackagesOptionsType = Type.GetTypeFromCLSID(FindPackagesOptionsClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static readonly Type CreateCompositePackageCatalogOptionsType = Type.GetTypeFromCLSID(CreateCompositePackageCatalogOptionsClsid); + private static readonly Type? CreateCompositePackageCatalogOptionsType = Type.GetTypeFromCLSID(CreateCompositePackageCatalogOptionsClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static readonly Type InstallOptionsType = Type.GetTypeFromCLSID(InstallOptionsClsid); + private static readonly Type? InstallOptionsType = Type.GetTypeFromCLSID(InstallOptionsClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static readonly Type UninstallOptionsType = Type.GetTypeFromCLSID(UninstallOptionsClsid); + private static readonly Type? UninstallOptionsType = Type.GetTypeFromCLSID(UninstallOptionsClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static readonly Type PackageMatchFilterType = Type.GetTypeFromCLSID(PackageMatchFilterClsid); + private static readonly Type? PackageMatchFilterType = Type.GetTypeFromCLSID(PackageMatchFilterClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static readonly Type DownloadOptionsType = Type.GetTypeFromCLSID(DownloadOptionsClsid); + private static readonly Type? DownloadOptionsType = Type.GetTypeFromCLSID(DownloadOptionsClsid); private static readonly Guid PackageManagerIid = Guid.Parse("B375E3B9-F2E0-5C93-87A7-B67497F7E593"); private static readonly Guid FindPackagesOptionsIid = Guid.Parse("A5270EDD-7DA7-57A3-BACE-F2593553561F"); @@ -157,9 +157,14 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static T Create<T>(Type type, in Guid iid) + private static T Create<T>(Type? type, in Guid iid) where T : new() { + if (type == null) + { + throw new ArgumentNullException(iid.ToString()); + } + if (Utilities.UsesInProcWinget) { var arch = RuntimeInformation.ProcessArchitecture; @@ -169,7 +174,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } string executingAssemblyLocation = Assembly.GetExecutingAssembly().Location; - string executingAssemblyDirectory = Path.Combine(Path.GetDirectoryName(executingAssemblyLocation), arch.ToString().ToLower()); + string executingAssemblyDirectory = Path.Combine(Path.GetDirectoryName(executingAssemblyLocation) !, arch.ToString().ToLower()); SetDllDirectoryW(executingAssemblyDirectory); @@ -183,7 +188,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } } - object instance = null; + object? instance = null; if (Utilities.ExecutingAsAdministrator) { @@ -206,6 +211,11 @@ namespace Microsoft.WinGet.Client.Engine.Helpers instance = Activator.CreateInstance(type); } + if (instance == null) + { + throw new ArgumentNullException(); + } + #if NET IntPtr pointer = Marshal.GetIUnknownForObject(instance); return MarshalInterface<T>.FromAbi(pointer); @@ -223,6 +233,6 @@ namespace Microsoft.WinGet.Client.Engine.Helpers [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool SetDllDirectoryW([MarshalAs(UnmanagedType.LPWStr)] string directory); + private static extern bool SetDllDirectoryW([MarshalAs(UnmanagedType.LPWStr)] string? directory); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs @@ -11,6 +11,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers using System.Runtime.InteropServices; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Client.Engine.Exceptions; using Windows.Foundation; /// <summary> @@ -21,7 +22,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { private static readonly Lazy<PackageManagerWrapper> Lazy = new (() => new PackageManagerWrapper()); - private PackageManager packageManager = null; + private PackageManager packageManager = null!; private PackageManagerWrapper() { @@ -111,6 +112,12 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private TReturn Execute<TReturn>(Func<TReturn> func, bool canRetry) { + if (Utilities.UsesInProcWinget && Utilities.ThreadIsSTA) + { + // If you failed here, then you didn't wrap your call in ManagementDeploymentCommand.Execute + throw new SingleThreadedApartmentException(); + } + bool stopRetry = false; while (true) { @@ -125,7 +132,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } catch (COMException ex) when (ex.HResult == ErrorCode.RpcServerUnavailable || ex.HResult == ErrorCode.RpcCallFailed) { - this.packageManager = null; + this.packageManager = null!; if (stopRetry || !canRetry) { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempDirectory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempDirectory.cs @@ -24,7 +24,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <param name="deleteIfExists">Delete directory if already exists. Default true.</param> /// <param name="cleanup">Deletes directory at disposing time. Default true.</param> public TempDirectory( - string directoryName = null, + string? directoryName = null, bool deleteIfExists = true, bool cleanup = true) { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempFile.cs @@ -26,9 +26,9 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <param name="content">Optional content. If not null or empty, creates file and writes to it.</param> /// <param name="cleanup">Deletes file at disposing time. Default true.</param> public TempFile( - string fileName = null, + string? fileName = null, bool deleteIfExists = true, - string content = null, + string? content = null, bool cleanup = true) { if (fileName is null) @@ -78,7 +78,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// Creates the file. /// </summary> /// <param name="content">Content.</param> - public void CreateFile(string content = null) + public void CreateFile(string? content = null) { if (content is null) { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetCLICommandResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetCLICommandResult.cs @@ -21,7 +21,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <param name="exitCode">Exit code.</param> /// <param name="stdOut">Standard output.</param> /// <param name="stdErr">Standard error.</param> - public WinGetCLICommandResult(string command, string parameters, int exitCode, string stdOut, string stdErr) + public WinGetCLICommandResult(string command, string? parameters, int exitCode, string stdOut, string stdErr) { this.Command = command; this.Parameters = parameters; @@ -38,7 +38,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <summary> /// Gets the parameters. /// </summary> - public string Parameters { get; private set; } + public string? Parameters { get; private set; } /// <summary> /// Gets the exit code. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WingetCLIWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WingetCLIWrapper.cs @@ -69,7 +69,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <param name="parameters">Parameters.</param> /// <param name="timeOut">Time out.</param> /// <returns>WinGetCommandResult.</returns> - public WinGetCLICommandResult RunCommand(string command, string parameters = null, int timeOut = 60000) + public WinGetCLICommandResult RunCommand(string command, string? parameters = null, int timeOut = 60000) { string args = command; if (!string.IsNullOrEmpty(parameters)) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WriteProgressAdapter.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WriteProgressAdapter.cs @@ -9,6 +9,7 @@ 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. @@ -17,16 +18,16 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { private readonly AutoResetEvent resetEvent = new (false); private readonly Queue<ProgressRecord> records = new (); - private readonly Cmdlet cmdlet; + private readonly PowerShellCmdlet pwshCmdlet; private volatile bool completed = false; /// <summary> /// Initializes a new instance of the <see cref="WriteProgressAdapter" /> class. /// </summary> - /// <param name="cmdlet">A <see cref="Cmdlet" /> instance.</param> - public WriteProgressAdapter(Cmdlet cmdlet) + /// <param name="pwshCmdlet">A <see cref="PowerShellCmdlet" /> instance.</param> + public WriteProgressAdapter(PowerShellCmdlet pwshCmdlet) { - this.cmdlet = cmdlet; + this.pwshCmdlet = pwshCmdlet; } /// <summary> @@ -83,7 +84,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { while (this.records.Count > 0) { - this.cmdlet.WriteProgress(this.records.Dequeue()); + 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 @@ -18,6 +18,7 @@ <TargetFrameworks>$(CoreFramework);$(DesktopFramework)</TargetFrameworks> <DocumentationFile>$(OutputPath)\Microsoft.WinGet.Client.Engine.xml</DocumentationFile> <SupportedOSPlatformVersion>10.0.18362.0</SupportedOSPlatformVersion> + <Nullable>enable</Nullable> </PropertyGroup> <PropertyGroup Condition="'$(UseProdCLSIDs)' == 'true'"> @@ -33,6 +34,11 @@ </PropertyGroup> <ItemGroup> + <Compile Include="..\CommonFiles\PowerShellCmdlet.cs" Link="PowerShellCmdlet.cs" /> + <Compile Include="..\CommonFiles\StreamType.cs" Link="StreamType.cs" /> + </ItemGroup> + + <ItemGroup> <PackageReference Include="Octokit" Version="4.0.3" /> <PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> @@ -42,8 +48,8 @@ <PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> <PackageReference Include="Microsoft.CSharp" Version="4.7.0" Condition="'$(TargetFramework)' == '$(DesktopFramework)'" /> - <PackageReference Include="Microsoft.Windows.CsWinRT" Version="1.6.5" Condition="'$(TargetFramework)' == '$(CoreFramework)'"/> - <PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.22000.196" PrivateAssets="all" Condition="'$(TargetFramework)' == '$(DesktopFramework)'"/> + <PackageReference Include="Microsoft.Windows.CsWinRT" Version="1.6.5" Condition="'$(TargetFramework)' == '$(CoreFramework)'" /> + <PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.22000.196" PrivateAssets="all" Condition="'$(TargetFramework)' == '$(DesktopFramework)'" /> </ItemGroup> <ItemGroup> @@ -84,21 +90,6 @@ <RuntimeIdentifier>win10</RuntimeIdentifier> </PropertyGroup> - <ItemGroup> - <Compile Update="Properties\Resources.Designer.cs"> - <DesignTime>True</DesignTime> - <AutoGen>True</AutoGen> - <DependentUpon>Resources.resx</DependentUpon> - </Compile> - </ItemGroup> - - <ItemGroup> - <EmbeddedResource Update="Properties\Resources.resx"> - <Generator>ResXFileCodeGenerator</Generator> - <LastGenOutput>Resources.Designer.cs</LastGenOutput> - </EmbeddedResource> - </ItemGroup> - <PropertyGroup Condition="'$(TargetFramework)' == '$(DesktopFramework)'"> <DefineConstants>$(DefineConstants);POWERSHELL_WINDOWS</DefineConstants> </PropertyGroup> @@ -115,6 +106,7 @@ <EmbeddedResource Update="Properties\Resources.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>Resources.Designer.cs</LastGenOutput> + <CustomToolNamespace>Microsoft.WinGet.Resources</CustomToolNamespace> </EmbeddedResource> </ItemGroup> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs @@ -114,7 +114,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects public PSPackageVersionInfo GetPackageVersionInfo(string version) { // get specific version that matches - PackageVersionId packageVersionId = this.AvailablePackageVersionIds.FirstOrDefault(x => x.Version == version); + PackageVersionId? packageVersionId = this.AvailablePackageVersionIds.FirstOrDefault(x => x.Version == version); if (packageVersionId != null) { return new PSPackageVersionInfo(this.CatalogPackageCOM.GetPackageVersionInfo(packageVersionId)); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs @@ -8,7 +8,7 @@ // </auto-generated> //------------------------------------------------------------------------------ -namespace Microsoft.WinGet.Client.Engine.Properties { +namespace Microsoft.WinGet.Resources { using System; @@ -70,6 +70,15 @@ namespace Microsoft.WinGet.Client.Engine.Properties { } /// <summary> + /// Looks up a localized string similar to Debug parameter not supported. + /// </summary> + internal static string DebugNotSupported { + get { + return ResourceManager.GetString("DebugNotSupported", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to An error occurred while searching for packages: {0}. /// </summary> internal static string FindPackagesExceptionMessage { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx @@ -224,4 +224,7 @@ <data name="RequiresAdminMessage" xml:space="preserve"> <value>This cmdlet requires administrator privileges to execute.</value> </data> + <data name="DebugNotSupported" xml:space="preserve"> + <value>Debug parameter not supported</value> + </data> </root> \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs @@ -1,442 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="AsyncCommand.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Configuration.Engine.Commands -{ - using System; - using System.Collections.Concurrent; - using System.Management.Automation; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.PowerShell.Commands; - using Microsoft.WinGet.Configuration.Engine.Exceptions; - using Microsoft.WinGet.Configuration.Engine.Resources; - using Microsoft.WinGet.SharedLib.Exceptions; - using Microsoft.WinGet.SharedLib.PolicySettings; - - /// <summary> - /// This is the base class for any command that performs async operations. - /// It supports running tasks in an MTA thread via RunOnMta. - /// If the thread is already running on an MTA it will executed it, otherwise - /// it will create a new MTA thread. - /// - /// Wait must be used to synchronously wait con the task. - /// </summary> - public abstract class AsyncCommand - { - private static readonly string[] WriteInformationTags = new string[] { "PSHOST" }; - - private readonly Thread originalThread; - - private readonly CancellationTokenSource source = new (); - private BlockingCollection<QueuedStream> queuedStreams = new (); - - private int progressActivityId = 0; - private ConcurrentDictionary<int, ProgressRecordType> progressRecords = new (); - - /// <summary> - /// Initializes a new instance of the <see cref="AsyncCommand"/> class. - /// </summary> - /// <param name="psCmdlet">PSCmdlet.</param> - public AsyncCommand(PSCmdlet psCmdlet) - { - // Passing Debug will make all the message actions to be Inquire. For async operations - // and the current queue message implementation this doesn't make sense. - // PowerShell will inquire for any message giving the impression that the task is - // paused, but the async operation is still running. - if (psCmdlet.MyInvocation.BoundParameters.ContainsKey("Debug")) - { - throw new NotSupportedException(Resources.DebugNotSupported); - } - - GroupPolicy groupPolicy = GroupPolicy.GetInstance(); - - if (!groupPolicy.IsEnabled(Policy.WinGet)) - { - throw new GroupPolicyException(Policy.WinGet, GroupPolicyFailureType.BlockedByPolicy); - } - - if (!groupPolicy.IsEnabled(Policy.Configuration)) - { - throw new GroupPolicyException(Policy.Configuration, GroupPolicyFailureType.BlockedByPolicy); - } - - if (!groupPolicy.IsEnabled(Policy.WinGetCommandLineInterfaces)) - { - throw new GroupPolicyException(Policy.WinGetCommandLineInterfaces, GroupPolicyFailureType.BlockedByPolicy); - } - - this.PsCmdlet = psCmdlet; - this.originalThread = Thread.CurrentThread; - } - - /// <summary> - /// The write stream type. - /// </summary> - public enum StreamType - { - /// <summary> - /// Debug. - /// </summary> - Debug, - - /// <summary> - /// Verbose. - /// </summary> - Verbose, - - /// <summary> - /// Warning. - /// </summary> - Warning, - - /// <summary> - /// Error. - /// </summary> - Error, - - /// <summary> - /// Progress. - /// </summary> - Progress, - - /// <summary> - /// Object. - /// </summary> - Object, - - /// <summary> - /// Information. - /// </summary> - Information, - } - - /// <summary> - /// Gets the base cmdlet. - /// </summary> - protected PSCmdlet PsCmdlet { get; private set; } - - /// <summary> - /// Request cancellation for this command. - /// </summary> - public void Cancel() - { - this.source.Cancel(); - } - - /// <summary> - /// Complete this operation. - /// </summary> - public virtual void Complete() - { - this.queuedStreams.CompleteAdding(); - } - - /// <summary> - /// Execute the delegate in a MTA thread. - /// </summary> - /// <param name="func">Function to execute.</param> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - internal Task RunOnMTA(Func<Task> func) - { - // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) - { - throw new InvalidOperationException(); - } - - if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) - { - this.Write(StreamType.Verbose, "Already running on MTA"); - return func(); - } - - this.Write(StreamType.Verbose, "Creating MTA thread"); - var tcs = new TaskCompletionSource(); - var thread = new Thread(() => - { - try - { - func().GetAwaiter().GetResult(); - tcs.SetResult(); - } - catch (Exception e) - { - tcs.SetException(e); - } - }); - - thread.SetApartmentState(ApartmentState.MTA); - thread.Start(); - return tcs.Task; - } - - /// <summary> - /// Execute the delegate in a MTA thread. - /// </summary> - /// <param name="func">Function to execute.</param> - /// <typeparam name="TResult">Return type of function.</typeparam> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - internal Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func) - { - // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) - { - throw new InvalidOperationException(); - } - - if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) - { - this.Write(StreamType.Verbose, "Already running on MTA"); - return func(); - } - - this.Write(StreamType.Verbose, "Creating MTA thread"); - var tcs = new TaskCompletionSource<TResult>(); - var thread = new Thread(() => - { - try - { - var result = func().GetAwaiter().GetResult(); - tcs.SetResult(result); - } - catch (Exception e) - { - tcs.SetException(e); - } - }); - - thread.SetApartmentState(ApartmentState.MTA); - thread.Start(); - return tcs.Task; - } - - /// <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> - /// <param name="writeCommand">The command that can write to PowerShell.</param> - internal void Wait(Task runningTask, AsyncCommand? writeCommand = null) - { - writeCommand ??= this; - - // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) - { - throw new InvalidOperationException(); - } - - do - { - this.ConsumeAndWriteStreams(writeCommand); - } - while (!(runningTask.IsCompleted && this.queuedStreams.IsCompleted)); - - if (runningTask.IsFaulted) - { - // If IsFaulted is true, the task's Status is equal to Faulted, - // and its Exception property will be non-null. - throw runningTask.Exception!; - } - } - - /// <summary> - /// Writes into the corresponding stream if running on the main thread. - /// Otherwise queue the message. - /// Is the caller responsibility to use the correct types. - /// </summary> - /// <param name="type">Stream type.</param> - /// <param name="data">Data.</param> - internal void Write(StreamType type, object data) - { - if (type == StreamType.Progress) - { - // Keep track of all progress activity. - ProgressRecord progressRecord = (ProgressRecord)data; - if (!this.progressRecords.TryAdd(progressRecord.ActivityId, progressRecord.RecordType)) - { - _ = this.progressRecords.TryUpdate(progressRecord.ActivityId, progressRecord.RecordType, ProgressRecordType.Completed); - } - } - - if (this.originalThread == Thread.CurrentThread) - { - this.CmdletWrite(type, data, this); - return; - } - - this.queuedStreams.Add(new QueuedStream(type, data)); - } - - /// <summary> - /// Write error with an exception. - /// </summary> - /// <param name="errorId">Error id.</param> - /// <param name="e">Exception.</param> - internal void WriteError(ErrorRecordErrorId errorId, Exception e) - { - this.Write( - StreamType.Error, - new ErrorRecord( - e, - errorId.ToString(), - ErrorCategory.WriteError, - null)); - } - - /// <summary> - /// Write error with a message. Create WriteErrorException. - /// </summary> - /// <param name="errorId">Error id.</param> - /// <param name="message">Message.</param> - /// <param name="e">Inner exception.</param> - internal void WriteError(ErrorRecordErrorId errorId, string message, Exception? e = null) - { - // The error record requires a exception that can't be null, but there's no requirement that it was thrown. - // If not specified use WriteErrorException. - this.Write( - StreamType.Error, - new ErrorRecord( - new WriteErrorException(message, e), - errorId.ToString(), - ErrorCategory.WriteError, - null)); - } - - /// <summary> - /// Helper to compute percentage and write progress for processing activities. - /// </summary> - /// <param name="activityId">Activity id.</param> - /// <param name="activity">The activity in progress.</param> - /// <param name="status">The status of the activity.</param> - /// <param name="completed">Number of completed actions.</param> - /// <param name="total">The expected total.</param> - internal void WriteProgressWithPercentage(int activityId, string activity, string status, int completed, int total) - { - double percentComplete = (double)completed / total; - var record = new ProgressRecord(activityId, activity, status) - { - RecordType = ProgressRecordType.Processing, - PercentComplete = (int)(100.0 * percentComplete), - }; - this.Write(StreamType.Progress, record); - } - - /// <summary> - /// Helper to complete progress records. - /// </summary> - /// <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) - { - var record = new ProgressRecord(activityId, activity, status) - { - RecordType = ProgressRecordType.Completed, - PercentComplete = 100, - }; - this.Write(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. - /// </summary> - /// <param name="writeCommand">The command that can write to PowerShell.</param> - internal void ConsumeAndWriteStreams(AsyncCommand writeCommand) - { - // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) - { - throw new InvalidOperationException(); - } - - // Take from the blocking collection until is completed. - try - { - while (true) - { - var queuedOutput = this.queuedStreams.Take(); - if (queuedOutput != null) - { - this.CmdletWrite(queuedOutput.Type, queuedOutput.Data, writeCommand); - } - } - } - catch (InvalidOperationException) - { - // We are done. - // An InvalidOperationException means that Take() was called on a completed collection. - } - } - - /// <summary> - /// Gets a new progress activity id. - /// </summary> - /// <returns>The new progress record id.</returns> - internal int GetNewProgressActivityId() - { - return Interlocked.Increment(ref this.progressActivityId); - } - - /// <summary> - /// Gets the cancellation token. - /// </summary> - /// <returns>CancellationToken.</returns> - protected CancellationToken GetCancellationToken() - { - return this.source.Token; - } - - private void CmdletWrite(StreamType streamType, object data, AsyncCommand writeCommand) - { - switch (streamType) - { - case StreamType.Debug: - writeCommand.PsCmdlet.WriteDebug((string)data); - break; - case StreamType.Verbose: - writeCommand.PsCmdlet.WriteVerbose((string)data); - break; - case StreamType.Warning: - writeCommand.PsCmdlet.WriteWarning((string)data); - break; - case StreamType.Error: - writeCommand.PsCmdlet.WriteError((ErrorRecord)data); - break; - case StreamType.Progress: - // If the activity is already completed don't write progress. - var progressRecord = (ProgressRecord)data; - if (this.progressRecords[progressRecord.ActivityId] == ProgressRecordType.Processing) - { - writeCommand.PsCmdlet.WriteProgress(progressRecord); - } - - break; - case StreamType.Object: - writeCommand.PsCmdlet.WriteObject(data); - break; - case StreamType.Information: - writeCommand.PsCmdlet.WriteInformation(data, WriteInformationTags); - break; - } - } - - private class QueuedStream - { - public QueuedStream(StreamType type, object data) - { - this.Type = type; - this.Data = data; - } - - public StreamType Type { get; } - - public object Data { get; } - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs @@ -7,6 +7,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { using System; + using System.Collections.Generic; using System.IO; using System.Linq; using System.Management.Automation; @@ -14,10 +15,12 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands using Microsoft.Management.Configuration; using Microsoft.Management.Configuration.Processor; using Microsoft.PowerShell; + using Microsoft.WinGet.Common.Command; using Microsoft.WinGet.Configuration.Engine.Exceptions; using Microsoft.WinGet.Configuration.Engine.Helpers; using Microsoft.WinGet.Configuration.Engine.PSObjects; - using Microsoft.WinGet.Configuration.Engine.Resources; + using Microsoft.WinGet.Resources; + using Microsoft.WinGet.SharedLib.PolicySettings; using Windows.Storage; using Windows.Storage.Streams; using WinRT; @@ -25,14 +28,14 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// <summary> /// Class that deals configuration commands. /// </summary> - public sealed class ConfigurationCommand : AsyncCommand + public sealed class ConfigurationCommand : PowerShellCmdlet { /// <summary> /// Initializes a new instance of the <see cref="ConfigurationCommand"/> class. /// </summary> /// <param name="psCmdlet">PSCmdlet.</param> public ConfigurationCommand(PSCmdlet psCmdlet) - : base(psCmdlet) + : base(psCmdlet, new HashSet<Policy> { Policy.WinGet, Policy.Configuration, Policy.WinGetCommandLineInterfaces }) { } @@ -87,20 +90,13 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands bool canUseTelemetry) { var openParams = new OpenConfigurationParameters( - this.PsCmdlet, configFile, modulePath, executionPolicy, canUseTelemetry); + this, configFile, modulePath, executionPolicy, canUseTelemetry); // Start task. var runningTask = this.RunOnMTA<PSConfigurationSet>( async () => { - try - { - return await this.OpenConfigurationSetAsync(openParams); - } - finally - { - this.Complete(); - } + return await this.OpenConfigurationSetAsync(openParams); }); this.Wait(runningTask); @@ -131,7 +127,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } finally { - this.Complete(); psConfigurationSet.DoneProcessing(); } @@ -229,7 +224,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } finally { - this.Complete(); psConfigurationSet.DoneProcessing(); } }); @@ -261,7 +255,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } finally { - this.Complete(); psConfigurationSet.DoneProcessing(); } }); @@ -344,7 +337,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } finally { - this.Complete(); psConfigurationSet.DoneProcessing(); } }); diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ApplyConfigurationException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ApplyConfigurationException.cs @@ -10,7 +10,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions using System.Collections.Generic; using Microsoft.Management.Configuration; using Microsoft.WinGet.Configuration.Engine.PSObjects; - using Microsoft.WinGet.Configuration.Engine.Resources; + using Microsoft.WinGet.Resources; /// <summary> /// Exception thrown when there's an error when configuration is applied. diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/GetDetailsException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/GetDetailsException.cs @@ -10,7 +10,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions using System.Collections.Generic; using Microsoft.Management.Configuration; using Microsoft.WinGet.Configuration.Engine.PSObjects; - using Microsoft.WinGet.Configuration.Engine.Resources; + using Microsoft.WinGet.Resources; /// <summary> /// Exception thrown while getting details. diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs @@ -9,7 +9,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions using System; using System.Text; using Microsoft.Management.Configuration; - using Microsoft.WinGet.Configuration.Engine.Resources; + using Microsoft.WinGet.Resources; /// <summary> /// Exception thrown when failed to open a configuration set. diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs @@ -7,7 +7,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { using Microsoft.Management.Configuration; - using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Common.Command; using Windows.Foundation; /// <summary> @@ -26,7 +26,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <param name="inProgressMessage">The message in the progress bar.</param> /// <param name="completeMessage">The activity complete message.</param> /// <param name="totalUnitsExpected">Total of units expected.</param> - public ApplyConfigurationSetProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + public ApplyConfigurationSetProgressOutput(PowerShellCmdlet cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) { } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationSetProgressOutputBase.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationSetProgressOutputBase.cs @@ -9,7 +9,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers using System; using System.Collections.Generic; using Microsoft.Management.Configuration; - using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Common.Command; using Windows.Foundation; /// <summary> @@ -19,7 +19,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <typeparam name="TProgressData">Progress data.</typeparam> internal abstract class ConfigurationSetProgressOutputBase<TOperationResult, TProgressData> { - private readonly AsyncCommand cmd; + private readonly PowerShellCmdlet cmd; private readonly int activityId; private readonly string activity; private readonly string inProgressMessage; @@ -35,7 +35,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <param name="inProgressMessage">The message in the progress bar.</param> /// <param name="completeMessage">The activity complete message.</param> /// <param name="totalUnitsExpected">Total of units expected.</param> - public ConfigurationSetProgressOutputBase(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + public ConfigurationSetProgressOutputBase(PowerShellCmdlet cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) { this.cmd = cmd; this.activityId = activityId; diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationUnitInformation.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationUnitInformation.cs @@ -13,7 +13,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers using System.Text; using Microsoft.Management.Configuration; using Microsoft.WinGet.Configuration.Engine.Extensions; - using Microsoft.WinGet.Configuration.Engine.Resources; + using Microsoft.WinGet.Resources; using Windows.Foundation.Collections; /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs @@ -7,7 +7,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { using Microsoft.Management.Configuration; - using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Common.Command; using Windows.Foundation; /// <summary> @@ -24,7 +24,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <param name="inProgressMessage">The message in the progress bar.</param> /// <param name="completeMessage">The activity complete message.</param> /// <param name="totalUnitsExpected">Total of units expected.</param> - public GetConfigurationSetDetailsProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + public GetConfigurationSetDetailsProgressOutput(PowerShellCmdlet cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) { } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs @@ -11,7 +11,8 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers using System.Management.Automation; using Microsoft.Management.Configuration.Processor; using Microsoft.PowerShell; - using Microsoft.WinGet.Configuration.Engine.Resources; + using Microsoft.WinGet.Common.Command; + using Microsoft.WinGet.Resources; /// <summary> /// The parameters used to open a configuration. @@ -25,19 +26,19 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <summary> /// Initializes a new instance of the <see cref="OpenConfigurationParameters"/> class. /// </summary> - /// <param name="psCmdlet">PsCmdlet caller.</param> + /// <param name="pwshCmdlet">PowerShellCmdlet.</param> /// <param name="file">The configuration file.</param> /// <param name="modulePath">The module path to use.</param> /// <param name="executionPolicy">Execution policy.</param> /// <param name="canUseTelemetry">If telemetry can be used.</param> public OpenConfigurationParameters( - PSCmdlet psCmdlet, + PowerShellCmdlet pwshCmdlet, string file, string modulePath, ExecutionPolicy executionPolicy, bool canUseTelemetry) { - this.ConfigFile = this.VerifyFile(file, psCmdlet); + this.ConfigFile = this.VerifyFile(file, pwshCmdlet); this.InitializeModulePath(modulePath); this.Policy = this.GetConfigurationProcessorPolicy(executionPolicy); this.CanUseTelemetry = canUseTelemetry; @@ -68,13 +69,13 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// </summary> public bool CanUseTelemetry { get; } - private string VerifyFile(string filePath, PSCmdlet psCmdlet) + private string VerifyFile(string filePath, PowerShellCmdlet pwshCmdlet) { if (!Path.IsPathRooted(filePath)) { filePath = Path.GetFullPath( Path.Combine( - psCmdlet.SessionState.Path.CurrentFileSystemLocation.Path, + pwshCmdlet.GetCurrentFileSystemLocation(), filePath)); } else diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/TestConfigurationSetProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/TestConfigurationSetProgressOutput.cs @@ -7,7 +7,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { using Microsoft.Management.Configuration; - using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Common.Command; using Windows.Foundation; /// <summary> @@ -26,7 +26,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <param name="inProgressMessage">The message in the progress bar.</param> /// <param name="completeMessage">The activity complete message.</param> /// <param name="totalUnitsExpected">Total of units expected.</param> - public TestConfigurationSetProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + public TestConfigurationSetProgressOutput(PowerShellCmdlet cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) { } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj @@ -25,6 +25,11 @@ </ItemGroup> <ItemGroup> + <Compile Include="..\CommonFiles\PowerShellCmdlet.cs" Link="PowerShellCmdlet.cs" /> + <Compile Include="..\CommonFiles\StreamType.cs" Link="StreamType.cs" /> + </ItemGroup> + + <ItemGroup> <PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> <PrivateAssets>all</PrivateAssets> @@ -58,6 +63,7 @@ <EmbeddedResource Update="Resources\Resources.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>Resources.Designer.cs</LastGenOutput> + <CustomToolNamespace>Microsoft.WinGet.Resources</CustomToolNamespace> </EmbeddedResource> </ItemGroup> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs @@ -7,7 +7,7 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects { using System.Threading.Tasks; - using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Common.Command; /// <summary> /// This is a wrapper object for asynchronous task for this module. @@ -22,7 +22,7 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects /// <param name="startCommand">The start command.</param> internal PSConfigurationJob( Task<PSApplyConfigurationSetResult> applyConfigTask, - AsyncCommand startCommand) + PowerShellCmdlet startCommand) { this.ApplyConfigurationTask = applyConfigTask; this.StartCommand = startCommand; @@ -36,6 +36,6 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects /// <summary> /// Gets the command that started async operation. /// </summary> - internal AsyncCommand StartCommand { get; private set; } + internal PowerShellCmdlet StartCommand { get; private set; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationProcessor.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationProcessor.cs @@ -7,12 +7,8 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects { using System; - using System.Management.Automation; using Microsoft.Management.Configuration; - using Microsoft.PowerShell.Commands; - using Microsoft.WinGet.Configuration.Engine.Commands; - using Microsoft.WinGet.Configuration.Engine.Exceptions; - using static Microsoft.WinGet.Configuration.Engine.Commands.AsyncCommand; + using Microsoft.WinGet.Common.Command; /// <summary> /// Creates configuration processor and set up diagnostic logging. @@ -26,7 +22,7 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects { private static readonly object CmdletLock = new (); - private AsyncCommand diagnosticCommand; + private PowerShellCmdlet diagnosticCommand; /// <summary> /// Initializes a new instance of the <see cref="PSConfigurationProcessor"/> class. @@ -34,7 +30,7 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects /// <param name="factory">Factory.</param> /// <param name="diagnosticCommand">AsyncCommand to use for diagnostics.</param> /// <param name="canUseTelemetry">If telemetry can be used.</param> - internal PSConfigurationProcessor(IConfigurationSetProcessorFactory factory, AsyncCommand diagnosticCommand, bool canUseTelemetry) + internal PSConfigurationProcessor(IConfigurationSetProcessorFactory factory, PowerShellCmdlet diagnosticCommand, bool canUseTelemetry) { this.Processor = new ConfigurationProcessor(factory); this.Processor.MinimumLevel = DiagnosticLevel.Verbose; @@ -53,7 +49,7 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects /// Updates the cmdlet that is used for diagnostics. /// </summary> /// <param name="newDiagnosticCommand">New diagnostic command.</param> - internal void UpdateDiagnosticCmdlet(AsyncCommand newDiagnosticCommand) + internal void UpdateDiagnosticCmdlet(PowerShellCmdlet newDiagnosticCommand) { lock (CmdletLock) { @@ -65,13 +61,13 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects { try { - AsyncCommand asyncCommand = this.diagnosticCommand; - if (asyncCommand != null) + PowerShellCmdlet pwshCmdlet = this.diagnosticCommand; + if (pwshCmdlet != null) { // Printing each diagnostic error in their own equivalent stream is too noisy. // If users want them they have to specify -Verbose. string tag = $"[Diagnostic{diagnosticInformation.Level}] "; - asyncCommand.Write(StreamType.Verbose, $"{tag}{diagnosticInformation.Message}"); + pwshCmdlet.Write(StreamType.Verbose, $"{tag}{diagnosticInformation.Message}"); } } catch (Exception) diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSUnitResult.cs @@ -8,7 +8,7 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects { using Microsoft.Management.Configuration; using Microsoft.WinGet.Configuration.Engine.Exceptions; - using Microsoft.WinGet.Configuration.Engine.Resources; + using Microsoft.WinGet.Resources; /// <summary> /// Unit result. diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs @@ -8,7 +8,7 @@ // </auto-generated> //------------------------------------------------------------------------------ -namespace Microsoft.WinGet.Configuration.Engine.Resources { +namespace Microsoft.WinGet.Resources { using System;