commit fee1b43faa7beca8c4ac44a77c1e07c5379a21ab parent 7eca519d03d0dceb14cd30c3a430071153c07913 Author: Ruben Guerrero <rubengu@microsoft.com> Date: Mon, 8 May 2023 17:19:11 -0700 Queue write operations for Start-* cmdlets and hook up diagnostics (#3222) When a Start-* cmdlet starts its operation it won't write to any PowerShell stream because control is returned back to the user almost immediately. This changes queue messages to be then displayed at Continue-* time. This way, we will have all the messages in the appropriate order. It applies for WriteDebug, WriteVerbose, WriteWarning, WriteError and WriteProgress. WriteProgress will only be called when the activity hasn't been completed. A caller must use AsyncCommand.GetNewProgressActivityId to generate a new activity id. Also enables telemetry and OnDiagnostic events Diffstat:
11 files changed, 404 insertions(+), 60 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -267,6 +267,7 @@ objbase objidl ofile osfhandle +OPTOUT Outptr packageinuse packageinusebyapplication diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs @@ -33,9 +33,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - CancellationTokenSource source = new (); - - var configCommand = new ConfigurationCommand(this, source.Token); + var configCommand = new ConfigurationCommand(this); configCommand.Continue(this.ConfigurationJob); } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs @@ -20,6 +20,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets public sealed class GetWinGetConfigurationCmdlet : PSCmdlet { private ExecutionPolicy executionPolicy = ExecutionPolicy.Undefined; + private bool canUseTelemetry = true; /// <summary> /// Gets or sets the configuration file. @@ -35,6 +36,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets protected override void BeginProcessing() { this.executionPolicy = Utilities.GetExecutionPolicy(); + this.canUseTelemetry = Utilities.CanUseTelemetry(); } /// <summary> @@ -42,10 +44,8 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - CancellationTokenSource source = new (); - - var configCommand = new ConfigurationCommand(this, source.Token); - configCommand.Get(this.File, this.executionPolicy); + var configCommand = new ConfigurationCommand(this); + configCommand.Get(this.File, this.executionPolicy, this.canUseTelemetry); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs @@ -32,9 +32,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - CancellationTokenSource source = new (); - - var configCommand = new ConfigurationCommand(this, source.Token); + var configCommand = new ConfigurationCommand(this); configCommand.GetDetails(this.Set); } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs @@ -46,9 +46,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - CancellationTokenSource source = new (); - - var configCommand = new ConfigurationCommand(this, source.Token, false); + var configCommand = new ConfigurationCommand(this); configCommand.Apply(this.Set); } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs @@ -7,7 +7,6 @@ namespace Microsoft.WinGet.Configuration.Cmdlets { using System.Management.Automation; - using System.Threading; using Microsoft.WinGet.Configuration.Engine.Commands; using Microsoft.WinGet.Configuration.Engine.PSObjects; @@ -46,9 +45,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - CancellationTokenSource source = new (); - - var configCommand = new ConfigurationCommand(this, source.Token, false); + var configCommand = new ConfigurationCommand(this, canWriteToStream: false); configCommand.StartApply(this.Set); } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Utilities.cs @@ -6,6 +6,7 @@ namespace Microsoft.WinGet.Configuration.Helpers { + using System; using System.Linq; using System.Management.Automation; using Microsoft.PowerShell; @@ -24,5 +25,49 @@ namespace Microsoft.WinGet.Configuration.Helpers var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); return ps.AddCommand("Get-ExecutionPolicy").Invoke<ExecutionPolicy>().First(); } + + /// <summary> + /// Determine if telemetry can be used. It follows the same telemetry rules as PowerShell. + /// To opt-out of this telemetry, set the environment variable $env:POWERSHELL_TELEMETRY_OPTOUT to true, yes, or 1. + /// This method is the same as GetEnvironmentVariableAsBool from PowerShell but only for POWERSHELL_TELEMETRY_OPTOUT. + /// </summary> + /// <returns>If telemetry can be used.</returns> + public static bool CanUseTelemetry() + { + var str = Environment.GetEnvironmentVariable("POWERSHELL_TELEMETRY_OPTOUT"); + if (string.IsNullOrEmpty(str)) + { + return true; + } + + var boolStr = str.AsSpan(); + + if (boolStr.Length == 1) + { + if (boolStr[0] == '1') + { + return false; + } + } + + if (boolStr.Length == 3 && + (boolStr[0] == 'y' || boolStr[0] == 'Y') && + (boolStr[1] == 'e' || boolStr[1] == 'E') && + (boolStr[2] == 's' || boolStr[2] == 'S')) + { + return false; + } + + if (boolStr.Length == 4 && + (boolStr[0] == 't' || boolStr[0] == 'T') && + (boolStr[1] == 'r' || boolStr[1] == 'R') && + (boolStr[2] == 'u' || boolStr[2] == 'U') && + (boolStr[3] == 'e' || boolStr[3] == 'E')) + { + return false; + } + + return true; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs @@ -7,6 +7,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { using System; + using System.Collections.Concurrent; using System.Management.Automation; using System.Threading; using System.Threading.Tasks; @@ -33,26 +34,40 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands private readonly SemaphoreSlim semaphore = new (1, 1); private readonly ManualResetEventSlim mainThreadActionReady = new (false); private readonly ManualResetEventSlim mainThreadActionCompleted = new (false); - private readonly CancellationToken cancellationToken; + + private readonly CancellationTokenSource source = new (); private readonly bool isDebugBounded; private Action? mainThreadAction = null; private bool canWriteToStream; + private CancellationToken cancellationToken; + private ConcurrentQueue<QueuedOutputStream> queuedOutputStreams = 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> - /// <param name="cancellationToken">Cancellation token.</param> /// <param name="canWriteToStream">If the command can write to stream.</param> - public AsyncCommand(PSCmdlet psCmdlet, CancellationToken cancellationToken, bool canWriteToStream) + public AsyncCommand(PSCmdlet psCmdlet, bool canWriteToStream) { this.PsCmdlet = psCmdlet; this.originalThread = Thread.CurrentThread; - this.cancellationToken = cancellationToken; this.isDebugBounded = this.PsCmdlet.MyInvocation.BoundParameters.ContainsKey("Debug"); this.canWriteToStream = canWriteToStream; + this.cancellationToken = this.source.Token; + } + + private enum OutputStreamType + { + Debug, + Verbose, + Warning, + Error, + Progress, } /// <summary> @@ -87,11 +102,19 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } /// <summary> + /// Cancel this operation. + /// </summary> + public virtual void Cancel() + { + this.source.Cancel(); + } + + /// <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> - public Task RunOnMTA(Func<Task> func) + internal Task RunOnMTA(Func<Task> func) { // This must be called in the main thread. if (this.originalThread != Thread.CurrentThread) @@ -131,7 +154,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// <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> - public Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func) + internal Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func) { // This must be called in the main thread. if (this.originalThread != Thread.CurrentThread) @@ -169,7 +192,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// Waits for the task to be completed. This MUST be called from the main thread. /// </summary> /// <param name="runningTask">Task to wait for.</param> - public void Wait(Task runningTask) + internal void Wait(Task runningTask) { // This must be called in the main thread. if (this.originalThread != Thread.CurrentThread) @@ -177,7 +200,8 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands throw new InvalidOperationException(); } - this.canWriteToStream = true; + this.Flush(); + do { // Wait for the running task to be completed or if there's @@ -218,16 +242,18 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// sets it to the main thread action and wait for it to be executed. /// </summary> /// <param name="text">Debug text.</param> - public void WriteDebug(string text) + internal void WriteDebug(string text) { - if (!this.CanWriteToStream) + // Don't do context switch if no need. + if (!this.isDebugBounded) { return; } - // Don't do context switch if no need. - if (!this.isDebugBounded) + if (!this.CanWriteToStream) { + this.queuedOutputStreams.Enqueue( + new QueuedOutputStream(OutputStreamType.Debug, text)); return; } @@ -251,15 +277,51 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } /// <summary> - /// Calls cmdlet WriteDebug. + /// Calls cmdlet WriteVerbose. + /// If its executed on the main thread calls it directly. Otherwise + /// sets it to the main thread action and wait for it to be executed. + /// </summary> + /// <param name="text">Verbose text.</param> + internal void WriteVerbose(string text) + { + if (!this.CanWriteToStream) + { + this.queuedOutputStreams.Enqueue( + new QueuedOutputStream(OutputStreamType.Verbose, text)); + return; + } + + if (this.originalThread == Thread.CurrentThread) + { + this.PsCmdlet.WriteVerbose(text); + return; + } + + try + { + this.WaitForOurTurn(); + this.mainThreadAction = () => this.PsCmdlet.WriteVerbose(text); + this.mainThreadActionReady.Set(); + this.WaitMainThreadActionCompletion(); + } + catch (Exception) + { + throw; + } + } + + /// <summary> + /// Calls cmdlet WriteWarning. /// If its executed on the main thread calls it directly. Otherwise /// sets it to the main thread action and wait for it to be executed. /// </summary> /// <param name="text">Warning text.</param> - public void WriteWarning(string text) + internal void WriteWarning(string text) { if (!this.CanWriteToStream) { + this.queuedOutputStreams.Enqueue( + new QueuedOutputStream(OutputStreamType.Warning, text)); return; } @@ -283,10 +345,82 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } /// <summary> + /// Calls cmdlet WriteError. + /// If its executed on the main thread calls it directly. Otherwise + /// sets it to the main thread action and wait for it to be executed. + /// </summary> + /// <param name="errorRecord">Error record.</param> + internal void WriteError(ErrorRecord errorRecord) + { + if (!this.CanWriteToStream) + { + this.queuedOutputStreams.Enqueue( + new QueuedOutputStream(OutputStreamType.Error, errorRecord)); + return; + } + + if (this.originalThread == Thread.CurrentThread) + { + this.PsCmdlet.WriteError(errorRecord); + return; + } + + try + { + this.WaitForOurTurn(); + this.mainThreadAction = () => this.PsCmdlet.WriteError(errorRecord); + this.mainThreadActionReady.Set(); + this.WaitMainThreadActionCompletion(); + } + catch (Exception) + { + throw; + } + } + + /// <summary> + /// Calls cmdlet WriteProgress. + /// </summary> + /// <param name="progressRecord">Progress record.</param> + internal void WriteProgress(ProgressRecord progressRecord) + { + // Keep track of all progress activity. + if (!this.progressRecords.TryAdd(progressRecord.ActivityId, progressRecord.RecordType)) + { + _ = this.progressRecords.TryUpdate(progressRecord.ActivityId, progressRecord.RecordType, ProgressRecordType.Completed); + } + + if (!this.CanWriteToStream) + { + this.queuedOutputStreams.Enqueue( + new QueuedOutputStream(OutputStreamType.Progress, progressRecord)); + return; + } + + if (this.originalThread == Thread.CurrentThread) + { + this.PsCmdlet.WriteProgress(progressRecord); + return; + } + + try + { + this.WaitForOurTurn(); + this.mainThreadAction = () => this.PsCmdlet.WriteProgress(progressRecord); + this.mainThreadActionReady.Set(); + this.WaitMainThreadActionCompletion(); + } + catch (Exception) + { + throw; + } + } + + /// <summary> /// Calls cmdlet WriteObject. /// </summary> /// <param name="obj">Object to write.</param> - public void WriteObject(object obj) + internal void WriteObject(object obj) { if (this.originalThread == Thread.CurrentThread) { @@ -307,6 +441,72 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } } + /// <summary> + /// Enable writing to pwsh streams and flush all the queued stream. + /// This method must be called in the original thread. + /// WARNING: You must only call this when the task is completed or in Wait. + /// </summary> + internal void Flush() + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + this.canWriteToStream = true; + + // At this point, no new messages should be added to the queue and we are in the main thread. + // Any non completed async operation will now wait for the main thread, so be sure to cancel + // if anything goes wrong. + try + { + while (this.queuedOutputStreams.TryDequeue(out var queuedOutput)) + { + if (queuedOutput != null) + { + switch (queuedOutput.Type) + { + case OutputStreamType.Debug: + this.WriteDebug((string)queuedOutput.Data); + break; + case OutputStreamType.Verbose: + this.WriteVerbose((string)queuedOutput.Data); + break; + case OutputStreamType.Warning: + this.WriteWarning((string)queuedOutput.Data); + break; + case OutputStreamType.Error: + this.WriteError((ErrorRecord)queuedOutput.Data); + break; + case OutputStreamType.Progress: + // If the activity is already completed don't write progress. + var progressRecord = (ProgressRecord)queuedOutput.Data; + if (this.progressRecords[progressRecord.ActivityId] == ProgressRecordType.Processing) + { + this.WriteProgress(progressRecord); + } + + break; + } + } + } + } + catch (Exception) + { + this.Cancel(); + } + } + + /// <summary> + /// Gets a new progress activity id. + /// </summary> + /// <returns>The new progress record id.</returns> + internal int GetNewProgressActivityId() + { + return Interlocked.Increment(ref this.progressActivityId); + } + private void WaitForOurTurn() { this.semaphore.Wait(this.cancellationToken); @@ -323,5 +523,18 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands this.semaphore.Release(); } + + private class QueuedOutputStream + { + public QueuedOutputStream(OutputStreamType type, object data) + { + this.Type = type; + this.Data = data; + } + + public OutputStreamType 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 @@ -9,7 +9,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands using System; using System.IO; using System.Management.Automation; - using System.Threading; using System.Threading.Tasks; using Microsoft.Management.Configuration; using Microsoft.Management.Configuration.Processor; @@ -27,10 +26,9 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// Initializes a new instance of the <see cref="ConfigurationCommand"/> class. /// </summary> /// <param name="psCmdlet">PSCmdlet.</param> - /// <param name="cancellationToken">Cancellation token.</param> /// <param name="canWriteToStream">If the command can write to stream.</param> - public ConfigurationCommand(PSCmdlet psCmdlet, CancellationToken cancellationToken, bool canWriteToStream = true) - : base(psCmdlet, cancellationToken, canWriteToStream) + public ConfigurationCommand(PSCmdlet psCmdlet, bool canWriteToStream = true) + : base(psCmdlet, canWriteToStream) { } @@ -39,7 +37,8 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// </summary> /// <param name="configFile">Configuration file path.</param> /// <param name="executionPolicy">Execution policy.</param> - public void Get(string configFile, ExecutionPolicy executionPolicy) + /// <param name="canUseTelemetry">If telemetry can be used.</param> + public void Get(string configFile, ExecutionPolicy executionPolicy, bool canUseTelemetry) { if (!Path.IsPathRooted(configFile)) { @@ -56,7 +55,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands // Start task. var runningTask = this.RunOnMTA<PSConfigurationSet>( - async () => await this.OpenConfigurationSetAsync(configFile, executionPolicy)); + async () => await this.OpenConfigurationSetAsync(configFile, executionPolicy, canUseTelemetry)); this.Wait(runningTask); this.WriteObject(runningTask.Result); @@ -68,6 +67,8 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// <param name="psConfigurationSet">PSConfigurationSet.</param> public void GetDetails(PSConfigurationSet psConfigurationSet) { + psConfigurationSet.PsProcessor.UpdateDiagnosticCmdlet(this); + if (!psConfigurationSet.HasDetails) { if (!psConfigurationSet.CanProcess()) @@ -138,6 +139,9 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { if (psConfigurationJob.ConfigurationTask.IsCompleted) { + // It is safe to print all output. + psConfigurationJob.StartCommand.Flush(); + this.WriteDebug("The task was completed before waiting"); if (psConfigurationJob.ConfigurationTask.IsCompletedSuccessfully) { @@ -165,7 +169,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands this.WriteObject(psConfigurationJob.ConfigurationTask.Result); } - private ConfigurationProcessor CreateConfigurationProcessor(ExecutionPolicy executionPolicy) + private PSConfigurationProcessor CreateConfigurationProcessor(ExecutionPolicy executionPolicy, bool canUseTelemetry) { var properties = new ConfigurationProcessorFactoryProperties(); properties.Policy = this.GetConfigurationProcessorPolicy(executionPolicy); @@ -173,24 +177,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands var factory = new ConfigurationSetProcessorFactory( ConfigurationProcessorType.Default, properties); - var processor = new ConfigurationProcessor(factory); - - // TODO: set up logging and telemetry. - ////processor.MinimumLevel = DiagnosticLevel.Error; - ////processor.Caller = "ConfigurationModule"; - ////processor.ActivityIdentifier = Guid.NewGuid(); - ////processor.GenerateTelemetryEvents = false; - ////processor.Diagnostics; - - return processor; + return new PSConfigurationProcessor(factory, this, canUseTelemetry); } - private async Task<PSConfigurationSet> OpenConfigurationSetAsync(string configFile, ExecutionPolicy executionPolicy) + private async Task<PSConfigurationSet> OpenConfigurationSetAsync(string configFile, ExecutionPolicy executionPolicy, bool canUseTelemetry) { - var processor = this.CreateConfigurationProcessor(executionPolicy); + var psProcessor = this.CreateConfigurationProcessor(executionPolicy, canUseTelemetry); var stream = await FileRandomAccessStream.OpenAsync(configFile, FileAccessMode.Read); - OpenConfigurationSetResult openResult = await processor.OpenConfigurationSetAsync(stream); + OpenConfigurationSetResult openResult = await psProcessor.Processor.OpenConfigurationSetAsync(stream); if (openResult.Set is null) { // TODO: throw better exception. @@ -205,11 +200,13 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands set.Origin = Path.GetDirectoryName(configFile); set.Path = configFile; - return new PSConfigurationSet(processor, set); + return new PSConfigurationSet(psProcessor, set); } private PSConfigurationJob StartApplyInternal(PSConfigurationSet psConfigurationSet) { + psConfigurationSet.PsProcessor.UpdateDiagnosticCmdlet(this); + var runningTask = this.RunOnMTA<PSConfigurationSet>( async () => { @@ -236,7 +233,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands await this.GetSetDetailsAsync(psConfigurationSet); } - var processor = psConfigurationSet.Processor; + var processor = psConfigurationSet.PsProcessor.Processor; var set = psConfigurationSet.Set; // TODO: implement progress @@ -247,7 +244,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands private async Task<PSConfigurationSet> GetSetDetailsAsync(PSConfigurationSet psConfigurationSet) { - var processor = psConfigurationSet.Processor; + var processor = psConfigurationSet.PsProcessor.Processor; var set = psConfigurationSet.Set; if (set.ConfigurationUnits.Count == 0) diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationProcessor.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationProcessor.cs @@ -0,0 +1,97 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSConfigurationProcessor.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +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; + + /// <summary> + /// Creates configuration processor and set up diagnostic logging. + /// If this object is the input of another cmdlet and the cmdlet is not a + /// long running task (aka not Continue-*) for now the caller is responsible + /// of updating the AsyncCommand of this object. + /// In the future we can implement a singleton that handles all the signaling + /// for main thread actions. + /// </summary> + public class PSConfigurationProcessor + { + private static readonly object CmdletLock = new (); + + private AsyncCommand diagnosticCommand; + + /// <summary> + /// Initializes a new instance of the <see cref="PSConfigurationProcessor"/> class. + /// </summary> + /// <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) + { + this.Processor = new ConfigurationProcessor(factory); + this.Processor.MinimumLevel = DiagnosticLevel.Verbose; + this.Processor.Caller = "Microsoft.WinGet.Configuration"; + this.Processor.Diagnostics += (sender, args) => this.LogConfigurationDiagnostics(args); + this.Processor.GenerateTelemetryEvents = canUseTelemetry; + this.diagnosticCommand = diagnosticCommand; + } + + /// <summary> + /// Gets the ConfigurationProcessor. + /// </summary> + internal ConfigurationProcessor Processor { get; private set; } + + /// <summary> + /// Updates the cmdlet that is used for diagnostics. + /// </summary> + /// <param name="newDiagnosticCommand">New diagnostic command.</param> + internal void UpdateDiagnosticCmdlet(AsyncCommand newDiagnosticCommand) + { + lock (CmdletLock) + { + this.diagnosticCommand = newDiagnosticCommand; + } + } + + private void LogConfigurationDiagnostics(DiagnosticInformation diagnosticInformation) + { + try + { + // This is expensive. + AsyncCommand asyncCommand = this.diagnosticCommand; + switch (diagnosticInformation.Level) + { + // PowerShell doesn't have critical and critical isn't an error. + case DiagnosticLevel.Critical: + case DiagnosticLevel.Warning: + asyncCommand.WriteWarning(diagnosticInformation.Message); + return; + case DiagnosticLevel.Error: + // TODO: The error record requires a exception that can't be null, but there's no requirement + // that it was thrown. + asyncCommand.WriteError(new ErrorRecord( + new WriteErrorException(), + "ConfigurationDiagnosticError", + ErrorCategory.WriteError, + diagnosticInformation.Message)); + return; + case DiagnosticLevel.Verbose: + case DiagnosticLevel.Informational: + default: + asyncCommand.WriteDebug(diagnosticInformation.Message); + return; + } + } + catch (Exception) + { + // Please don't throw here. + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs @@ -20,11 +20,11 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects /// <summary> /// Initializes a new instance of the <see cref="PSConfigurationSet"/> class. /// </summary> - /// <param name="processor">The configuration processor.</param> + /// <param name="psProcessor">The configuration processor wrapper.</param> /// <param name="set">The configuration set.</param> - internal PSConfigurationSet(ConfigurationProcessor processor, ConfigurationSet set) + internal PSConfigurationSet(PSConfigurationProcessor psProcessor, ConfigurationSet set) { - this.Processor = processor; + this.PsProcessor = psProcessor; this.Set = set; } @@ -84,9 +84,9 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects } /// <summary> - /// Gets the ConfigurationProcessor. + /// Gets the PSConfigurationProcessor. /// </summary> - internal ConfigurationProcessor Processor { get; private set; } + internal PSConfigurationProcessor PsProcessor { get; private set; } /// <summary> /// Gets the ConfigurationSet.