commit d68357d113395d27b4eb35b05483f6c965006d1b parent f71241785b0463c8783cca1c7d24f3a871f81a5a Author: Ruben Guerrero <rubengu@microsoft.com> Date: Thu, 18 May 2023 15:31:09 -0700 Microsoft.WinGet.Configuration messages (#3242) This PR starts printing information messages for Get-WingetConfiguration and Get-WingetConfigurationDetails. The messages are similar to what winget is showing. Also: Add resource file with necessary configuration messages. These are mostly the same as the one winget uses. Modify AsyncCommand to always use a BlockingCollection for storing messages. The messages are written to PowerShell when a cmdlet waits for the task to complete or a Complete-* cmdlet executes and the task is already completed. This completely removes the context switch (which might come back if we ever require a ShouldContinue/ShouldProgress in the middle of the task) Diffstat:
19 files changed, 2152 insertions(+), 365 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -14,6 +14,7 @@ apfn apicontract apiset appinstallertest +applic appname argumentlist ARMNT diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs @@ -10,7 +10,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands using System.Management.Automation; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; - using Microsoft.WinGet.Client.Engine.Extensions; using Microsoft.WinGet.Client.Engine.Helpers; using Microsoft.WinGet.Client.Engine.Properties; using Microsoft.WinGet.Client.Engine.PSObjects; diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs @@ -7,7 +7,6 @@ namespace Microsoft.WinGet.Configuration.Cmdlets { using System.Management.Automation; - using System.Threading; using Microsoft.PowerShell; using Microsoft.WinGet.Configuration.Engine.Commands; using Microsoft.WinGet.Configuration.Helpers; diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs @@ -19,6 +19,8 @@ namespace Microsoft.WinGet.Configuration.Cmdlets [Cmdlet(VerbsLifecycle.Invoke, "WinGetConfiguration")] public sealed class InvokeWinGetConfigurationCmdlet : PSCmdlet { + private bool acceptedAgreements = false; + /// <summary> /// Gets or sets the configuration set. /// </summary> @@ -31,6 +33,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// <summary> /// Gets or sets a value indicating whether to accept the configuration agreements. /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter AcceptConfigurationAgreements { get; set; } /// <summary> @@ -38,7 +41,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void BeginProcessing() { - // TODO: if not agrementsAccepted print message with ShouldContinue. + this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool()); } /// <summary> @@ -46,8 +49,11 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - var configCommand = new ConfigurationCommand(this); - configCommand.Apply(this.Set); + if (this.acceptedAgreements) + { + 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 @@ -18,6 +18,8 @@ namespace Microsoft.WinGet.Configuration.Cmdlets [Cmdlet(VerbsLifecycle.Start, "WinGetConfiguration")] public sealed class StartWinGetConfigurationCmdlet : PSCmdlet { + private bool acceptedAgreements = false; + /// <summary> /// Gets or sets the configuration set. /// </summary> @@ -30,6 +32,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// <summary> /// Gets or sets a value indicating whether to accept the configuration agreements. /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter AcceptConfigurationAgreements { get; set; } /// <summary> @@ -37,7 +40,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void BeginProcessing() { - // TODO: if not agrementsAccepted print message with ShouldContinue. + this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool()); } /// <summary> @@ -45,8 +48,11 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - var configCommand = new ConfigurationCommand(this, canWriteToStream: false); - configCommand.StartApply(this.Set); + if (this.acceptedAgreements) + { + var configCommand = new ConfigurationCommand(this); + configCommand.StartApply(this.Set); + } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs @@ -11,6 +11,9 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands 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; /// <summary> /// This is the base class for any command that performs async operations. @@ -18,31 +21,18 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// If the thread is already running on an MTA it will executed it, otherwise /// it will create a new MTA thread. /// - /// Calling PSCmdlet functions to write into their stream from not the main thread will - /// throw an exception. - /// This class contains wrappers around those methods with synchronization mechanisms - /// to output the messages. - /// /// Wait must be used to synchronously wait con the task. /// </summary> public abstract class AsyncCommand { - private static readonly object CmdletLock = new (); + private static readonly string[] WriteInformationTags = new string[] { "PSHOST" }; private readonly Thread originalThread; - private readonly SemaphoreSlim semaphore = new (1, 1); - private readonly ManualResetEventSlim mainThreadActionReady = new (false); - private readonly ManualResetEventSlim mainThreadActionCompleted = new (false); - 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 BlockingCollection<QueuedStream> queuedStreams = new (); private int progressActivityId = 0; private ConcurrentDictionary<int, ProgressRecordType> progressRecords = new (); @@ -51,23 +41,61 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// Initializes a new instance of the <see cref="AsyncCommand"/> class. /// </summary> /// <param name="psCmdlet">PSCmdlet.</param> - /// <param name="canWriteToStream">If the command can write to stream.</param> - public AsyncCommand(PSCmdlet psCmdlet, bool canWriteToStream) + 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); + } + this.PsCmdlet = psCmdlet; this.originalThread = Thread.CurrentThread; - this.isDebugBounded = this.PsCmdlet.MyInvocation.BoundParameters.ContainsKey("Debug"); - this.canWriteToStream = canWriteToStream; this.cancellationToken = this.source.Token; } - private enum OutputStreamType + /// <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> @@ -76,37 +104,11 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands protected PSCmdlet PsCmdlet { get; private set; } /// <summary> - /// Gets or sets a value indicating whether if writing to stream is blocked. - /// Writing to streams must be blocked for Start-* cmdlets. The Complete-* cmdlet counterpart - /// will enable writing to the stream when executed. - /// TODO: For now any messages before the Complete-* call get lost. We can add a ConcurrentQueue - /// to store message and flush them in or before the Wait call. + /// Complete this operation. /// </summary> - private bool CanWriteToStream + public virtual void Complete() { - get - { - lock (CmdletLock) - { - return this.canWriteToStream; - } - } - - set - { - lock (CmdletLock) - { - this.canWriteToStream = value; - } - } - } - - /// <summary> - /// Cancel this operation. - /// </summary> - public virtual void Cancel() - { - this.source.Cancel(); + this.queuedStreams.CompleteAdding(); } /// <summary> @@ -124,11 +126,11 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) { - this.WriteDebug("Already running on MTA"); + this.Write(StreamType.Verbose, "Already running on MTA"); return func(); } - this.WriteDebug("Creating MTA thread"); + this.Write(StreamType.Verbose, "Creating MTA thread"); var tcs = new TaskCompletionSource(); var thread = new Thread(() => { @@ -164,11 +166,11 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) { - this.WriteDebug("Already running on MTA"); + this.Write(StreamType.Verbose, "Already running on MTA"); return func(); } - this.WriteDebug("Creating MTA thread"); + this.Write(StreamType.Verbose, "Creating MTA thread"); var tcs = new TaskCompletionSource<TResult>(); var thread = new Thread(() => { @@ -200,33 +202,11 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands throw new InvalidOperationException(); } - this.Flush(); - do { - // Wait for the running task to be completed or if there's - // an action that needs to be executed in the main thread. - WaitHandle.WaitAny(new[] - { - this.mainThreadActionReady.WaitHandle, - ((IAsyncResult)runningTask).AsyncWaitHandle, - }); - - if (this.mainThreadActionReady.IsSet) - { - // Someone needs the main thread. - this.mainThreadActionReady.Reset(); - - if (this.mainThreadAction != null) - { - this.mainThreadAction(); - } - - // Done. - this.mainThreadActionCompleted.Set(); - } + this.ConsumeStreams(); } - while (!runningTask.IsCompleted); + while (!runningTask.IsCompleted && this.queuedStreams.IsCompleted); if (runningTask.IsFaulted) { @@ -237,216 +217,109 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } /// <summary> - /// Calls cmdlet WriteDebug. - /// 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">Debug text.</param> - internal void WriteDebug(string text) - { - // Don't do context switch if no need. - if (!this.isDebugBounded) - { - return; - } - - if (!this.CanWriteToStream) - { - this.queuedOutputStreams.Enqueue( - new QueuedOutputStream(OutputStreamType.Debug, text)); - return; - } - - if (this.originalThread == Thread.CurrentThread) - { - this.PsCmdlet.WriteDebug(text); - return; - } - - try - { - this.WaitForOurTurn(); - this.mainThreadAction = () => this.PsCmdlet.WriteDebug(text); - this.mainThreadActionReady.Set(); - this.WaitMainThreadActionCompletion(); - } - catch (Exception) - { - throw; - } - } - - /// <summary> - /// 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. + /// 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="text">Verbose text.</param> - internal void WriteVerbose(string text) + /// <param name="type">Stream type.</param> + /// <param name="data">Data.</param> + internal void Write(StreamType type, object data) { - if (!this.CanWriteToStream) + if (type == StreamType.Progress) { - this.queuedOutputStreams.Enqueue( - new QueuedOutputStream(OutputStreamType.Verbose, text)); - return; + // 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.PsCmdlet.WriteVerbose(text); + this.CmdletWrite(type, data); return; } - try - { - this.WaitForOurTurn(); - this.mainThreadAction = () => this.PsCmdlet.WriteVerbose(text); - this.mainThreadActionReady.Set(); - this.WaitMainThreadActionCompletion(); - } - catch (Exception) - { - throw; - } + this.queuedStreams.Add(new QueuedStream(type, data)); } /// <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. + /// Write error with an exception. /// </summary> - /// <param name="text">Warning text.</param> - internal void WriteWarning(string text) + /// <param name="errorId">Error id.</param> + /// <param name="e">Exception.</param> + internal void WriteError(ErrorRecordErrorId errorId, Exception e) { - if (!this.CanWriteToStream) - { - this.queuedOutputStreams.Enqueue( - new QueuedOutputStream(OutputStreamType.Warning, text)); - return; - } - - if (this.originalThread == Thread.CurrentThread) - { - this.PsCmdlet.WriteWarning(text); - return; - } - - try - { - this.WaitForOurTurn(); - this.mainThreadAction = () => this.PsCmdlet.WriteWarning(text); - this.mainThreadActionReady.Set(); - this.WaitMainThreadActionCompletion(); - } - catch (Exception) - { - throw; - } + this.Write( + StreamType.Error, + new ErrorRecord( + e, + errorId.ToString(), + ErrorCategory.WriteError, + null)); } /// <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. + /// Write error with a message. Create WriteErrorException. /// </summary> - /// <param name="errorRecord">Error record.</param> - internal void WriteError(ErrorRecord errorRecord) + /// <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) { - 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; - } + // 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> - /// Calls cmdlet WriteProgress. + /// Helper to compute percentage and write progress for processing activities. /// </summary> - /// <param name="progressRecord">Progress record.</param> - internal void WriteProgress(ProgressRecord progressRecord) + /// <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) { - // Keep track of all progress activity. - if (!this.progressRecords.TryAdd(progressRecord.ActivityId, progressRecord.RecordType)) + double percentComplete = (double)completed / total; + var record = new ProgressRecord(activityId, activity, status) { - _ = 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; - } + RecordType = ProgressRecordType.Processing, + PercentComplete = (int)(100.0 * percentComplete), + }; + this.Write(StreamType.Progress, record); } /// <summary> - /// Calls cmdlet WriteObject. + /// Helper to complete progress records. /// </summary> - /// <param name="obj">Object to write.</param> - internal void WriteObject(object obj) + /// <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) { - if (this.originalThread == Thread.CurrentThread) - { - this.PsCmdlet.WriteObject(obj); - return; - } - - try + var record = new ProgressRecord(activityId, activity, status) { - this.WaitForOurTurn(); - this.mainThreadAction = () => this.PsCmdlet.WriteObject(obj); - this.mainThreadActionReady.Set(); - this.WaitMainThreadActionCompletion(); - } - catch (Exception) - { - throw; - } + RecordType = ProgressRecordType.Completed, + PercentComplete = 100, + }; + this.Write(StreamType.Progress, record); } /// <summary> - /// Enable writing to pwsh streams and flush all the queued stream. + /// 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> - internal void Flush() + internal void ConsumeStreams() { // This must be called in the main thread. if (this.originalThread != Thread.CurrentThread) @@ -454,47 +327,22 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands 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. + // Take from the blocking collection until is completed. try { - while (this.queuedOutputStreams.TryDequeue(out var queuedOutput)) + while (true) { + var queuedOutput = this.queuedStreams.Take(); 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; - } + this.CmdletWrite(queuedOutput.Type, queuedOutput.Data); } } } - catch (Exception) + catch (InvalidOperationException) { - this.Cancel(); + // We are done. + // An InvalidOperationException means that Take() was called on a completed collection. } } @@ -507,32 +355,49 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands return Interlocked.Increment(ref this.progressActivityId); } - private void WaitForOurTurn() + private void CmdletWrite(StreamType streamType, object data) { - this.semaphore.Wait(this.cancellationToken); - this.mainThreadActionCompleted.Reset(); - } - - private void WaitMainThreadActionCompletion() - { - WaitHandle.WaitAny(new[] - { - this.cancellationToken.WaitHandle, - this.mainThreadActionCompleted.WaitHandle, - }); + switch (streamType) + { + case StreamType.Debug: + this.PsCmdlet.WriteDebug((string)data); + break; + case StreamType.Verbose: + this.PsCmdlet.WriteVerbose((string)data); + break; + case StreamType.Warning: + this.PsCmdlet.WriteWarning((string)data); + break; + case StreamType.Error: + this.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) + { + this.PsCmdlet.WriteProgress(progressRecord); + } - this.semaphore.Release(); + break; + case StreamType.Object: + this.PsCmdlet.WriteObject(data); + break; + case StreamType.Information: + this.PsCmdlet.WriteInformation(data, WriteInformationTags); + break; + } } - private class QueuedOutputStream + private class QueuedStream { - public QueuedOutputStream(OutputStreamType type, object data) + public QueuedStream(StreamType type, object data) { this.Type = type; this.Data = data; } - public OutputStreamType Type { get; } + 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 @@ -13,7 +13,10 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands using Microsoft.Management.Configuration; using Microsoft.Management.Configuration.Processor; using Microsoft.PowerShell; + 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 Windows.Storage; using Windows.Storage.Streams; @@ -26,13 +29,47 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// Initializes a new instance of the <see cref="ConfigurationCommand"/> class. /// </summary> /// <param name="psCmdlet">PSCmdlet.</param> - /// <param name="canWriteToStream">If the command can write to stream.</param> - public ConfigurationCommand(PSCmdlet psCmdlet, bool canWriteToStream = true) - : base(psCmdlet, canWriteToStream) + public ConfigurationCommand(PSCmdlet psCmdlet) + : base(psCmdlet) { } /// <summary> + /// Verify user accept agreements. + /// </summary> + /// <param name="psCmdlet">PSCmdlet.</param> + /// <param name="hasAccepted">Has already accepted.</param> + /// <returns>If accepted.</returns> + public static bool ConfirmConfigurationProcessing(PSCmdlet psCmdlet, bool hasAccepted) + { + bool result = false; + if (!hasAccepted) + { + bool yesToAll = false; + bool noToAll = false; + result = psCmdlet.ShouldContinue(Resources.ConfigurationWarningPrompt, Resources.ConfigurationWarning, true, ref yesToAll, ref noToAll); + + if (yesToAll) + { + result = true; + } + else if (noToAll) + { + result = false; + } + } + else + { + // This way even if they set WarningActionPreference.Ignore we will still print the + // warning message if the agreements didn't get accepted. + psCmdlet.WriteWarning(Resources.ConfigurationWarning); + result = true; + } + + return result; + } + + /// <summary> /// Open a configuration set. /// </summary> /// <param name="configFile">Configuration file path.</param> @@ -40,25 +77,24 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// <param name="canUseTelemetry">If telemetry can be used.</param> public void Get(string configFile, ExecutionPolicy executionPolicy, bool canUseTelemetry) { - if (!Path.IsPathRooted(configFile)) - { - configFile = Path.GetFullPath( - Path.Combine( - this.PsCmdlet.SessionState.Path.CurrentFileSystemLocation.Path, - configFile)); - } - - if (!File.Exists(configFile)) - { - throw new FileNotFoundException(configFile); - } + configFile = this.VerifyFile(configFile); // Start task. var runningTask = this.RunOnMTA<PSConfigurationSet>( - async () => await this.OpenConfigurationSetAsync(configFile, executionPolicy, canUseTelemetry)); + async () => + { + try + { + return await this.OpenConfigurationSetAsync(configFile, executionPolicy, canUseTelemetry); + } + finally + { + this.Complete(); + } + }); this.Wait(runningTask); - this.WriteObject(runningTask.Result); + this.Write(StreamType.Object, runningTask.Result); } /// <summary> @@ -86,6 +122,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } finally { + this.Complete(); psConfigurationSet.DoneProcessing(); } @@ -97,10 +134,10 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } else { - this.WriteWarning("Details already obtained for this set"); + this.Write(StreamType.Warning, "Details already obtained for this set"); } - this.WriteObject(psConfigurationSet); + this.Write(StreamType.Object, psConfigurationSet); } /// <summary> @@ -111,7 +148,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { if (psConfigurationSet.Set.State == ConfigurationSetState.Completed) { - this.WriteWarning("Processing this set is completed"); + this.Write(StreamType.Warning, "Processing this set is completed"); return; } @@ -122,7 +159,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } var configurationJob = this.StartApplyInternal(psConfigurationSet); - this.WriteObject(configurationJob); + this.Write(StreamType.Object, configurationJob); } /// <summary> @@ -140,18 +177,18 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands if (psConfigurationJob.ConfigurationTask.IsCompleted) { // It is safe to print all output. - psConfigurationJob.StartCommand.Flush(); + psConfigurationJob.StartCommand.ConsumeStreams(); - this.WriteDebug("The task was completed before waiting"); + this.Write(StreamType.Verbose, "The task was completed before waiting"); if (psConfigurationJob.ConfigurationTask.IsCompletedSuccessfully) { - this.WriteDebug("Completed successfully"); - this.WriteObject(psConfigurationJob.ConfigurationTask.Result); + this.Write(StreamType.Verbose, "Completed successfully"); + this.Write(StreamType.Object, psConfigurationJob.ConfigurationTask.Result); return; } else if (psConfigurationJob.ConfigurationTask.IsFaulted) { - this.WriteDebug("Completed faulted before waiting"); + this.Write(StreamType.Verbose, "Completed faulted before waiting"); // Maybe just write error? throw psConfigurationJob.ConfigurationTask.Exception!; @@ -161,16 +198,41 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands this.ContinueHelper(psConfigurationJob); } + /// <summary> + /// Verifies file exists and return the full path, if not already. + /// </summary> + /// <param name="filePath">File path.</param> + /// <returns>Full path.</returns> + private string VerifyFile(string filePath) + { + if (!Path.IsPathRooted(filePath)) + { + filePath = Path.GetFullPath( + Path.Combine( + this.PsCmdlet.SessionState.Path.CurrentFileSystemLocation.Path, + filePath)); + } + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException(filePath); + } + + return filePath; + } + private void ContinueHelper(PSConfigurationJob psConfigurationJob) { // Signal the command that it can write to streams and wait for task. - this.WriteDebug("Waiting for task to complete"); + this.Write(StreamType.Verbose, "Waiting for task to complete"); psConfigurationJob.StartCommand.Wait(psConfigurationJob.ConfigurationTask); - this.WriteObject(psConfigurationJob.ConfigurationTask.Result); + this.Write(StreamType.Object, psConfigurationJob.ConfigurationTask.Result); } private PSConfigurationProcessor CreateConfigurationProcessor(ExecutionPolicy executionPolicy, bool canUseTelemetry) { + this.Write(StreamType.Information, Resources.ConfigurationInitializing); + var properties = new ConfigurationProcessorFactoryProperties(); properties.Policy = this.GetConfigurationProcessorPolicy(executionPolicy); @@ -184,12 +246,13 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { var psProcessor = this.CreateConfigurationProcessor(executionPolicy, canUseTelemetry); + this.Write(StreamType.Information, Resources.ConfigurationReadingConfigFile); var stream = await FileRandomAccessStream.OpenAsync(configFile, FileAccessMode.Read); + OpenConfigurationSetResult openResult = await psProcessor.Processor.OpenConfigurationSetAsync(stream); - if (openResult.Set is null) + if (openResult.ResultCode != null) { - // TODO: throw better exception. - throw new Exception($"Failed opening configuration set. Result 0x{openResult.ResultCode} at {openResult.Field}"); + throw new OpenConfigurationSetException(openResult, configFile); } var set = openResult.Set; @@ -216,6 +279,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands } finally { + this.Complete(); psConfigurationSet.DoneProcessing(); } @@ -229,15 +293,33 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { if (!psConfigurationSet.HasDetails) { - this.WriteDebug("Getting details for configuration set"); + this.Write(StreamType.Verbose, "Getting details for configuration set"); await this.GetSetDetailsAsync(psConfigurationSet); } var processor = psConfigurationSet.PsProcessor.Processor; var set = psConfigurationSet.Set; - // TODO: implement progress - _ = await processor.ApplySetAsync(set, ApplyConfigurationSetFlags.None); + var applyProgressOutput = new ApplyConfigurationSetProgressOutput( + this, + this.GetNewProgressActivityId(), + Resources.ConfigurationApply, + Resources.OperationInProgress, + Resources.OperationCompleted, + set.ConfigurationUnits.Count); + + var applyTask = processor.ApplySetAsync(set, ApplyConfigurationSetFlags.None); + applyTask.Progress = applyProgressOutput.Progress; + + try + { + var result = await applyTask; + applyProgressOutput.HandleUnreportedProgress(result); + } + finally + { + applyProgressOutput.CompleteProgress(); + } return psConfigurationSet; } @@ -246,19 +328,72 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { var processor = psConfigurationSet.PsProcessor.Processor; var set = psConfigurationSet.Set; + var totalUnitsCount = set.ConfigurationUnits.Count; - if (set.ConfigurationUnits.Count == 0) + if (totalUnitsCount == 0) { - this.WriteWarning("Configuration File Empty"); + this.Write(StreamType.Warning, Resources.ConfigurationFileEmpty); + return psConfigurationSet; } - // TODO: implement progress - _ = await processor.GetSetDetailsAsync(set, ConfigurationUnitDetailLevel.Catalog); + var detailsProgressOutput = new GetConfigurationSetDetailsProgressOutput( + this, + this.GetNewProgressActivityId(), + Resources.ConfigurationGettingDetails, + Resources.OperationInProgress, + Resources.OperationCompleted, + totalUnitsCount); + + var detailsTask = processor.GetSetDetailsAsync(set, ConfigurationUnitDetailLevel.Catalog); + detailsTask.Progress = detailsProgressOutput.Progress; + + try + { + var result = await detailsTask; + detailsProgressOutput.HandleUnits(result.UnitResults); + } + catch (Exception e) + { + this.WriteError( + ErrorRecordErrorId.ConfigurationDetailsError, + e); + } + finally + { + detailsProgressOutput.CompleteProgress(); + } + + if (detailsProgressOutput.UnitsShown == 0) + { + this.Write(StreamType.Warning, Resources.ConfigurationFailedToGetDetails); + foreach (var unit in set.ConfigurationUnits) + { + var information = new ConfigurationUnitInformation(unit); + this.Write(StreamType.Information, information.GetHeader()); + this.Write(StreamType.Information, information.GetInformation()); + } + } + else + { + psConfigurationSet.HasDetails = true; + } - psConfigurationSet.HasDetails = true; return psConfigurationSet; } + private void LogFailedGetConfigurationUnitDetails(ConfigurationUnit unit, ConfigurationUnitResultInformation resultInformation) + { + if (resultInformation.ResultCode != null) + { + string errorMessage = $"Failed to get unit details for {unit.UnitName} 0x{resultInformation.ResultCode.HResult:X}" + + $"{Environment.NewLine}Description: '{resultInformation.Description}'{Environment.NewLine}Details: '{resultInformation.Details}'"; + this.WriteError( + ErrorRecordErrorId.ConfigurationDetailsError, + errorMessage, + resultInformation.ResultCode); + } + } + private ConfigurationProcessorPolicy GetConfigurationProcessorPolicy(ExecutionPolicy policy) { return policy switch diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ErrorCodes.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Exceptions +{ + /// <summary> + /// This should match the ones in AppInstallerErrors.h. + /// </summary> + internal static class ErrorCodes + { +#pragma warning disable SA1600 // ElementsMustBeDocumented + internal const int WingetConfigErrorInvalidConfigurationFile = unchecked((int)0x8A15C001); + internal const int WingetConfigErrorInvalidYaml = unchecked((int)0x8A15C002); + internal const int WingetConfigErrorInvalidFieldType = unchecked((int)0x8A15C003); + internal const int WingetConfigErrorUnknownConfigurationFileVersion = unchecked((int)0x8A15C004); + internal const int WingetConfigErrorSetApplyFailed = unchecked((int)0x8A15C005); + internal const int WingetConfigErrorDuplicateIdentifier = unchecked((int)0x8A15C006); + internal const int WingetConfigErrorMissingDependency = unchecked((int)0x8A15C007); + internal const int WingetConfigErrorDependencyUnsatisfied = unchecked((int)0x8A15C008); + internal const int WingetConfigErrorAssertionFailed = unchecked((int)0x8A15C009); + internal const int WingetConfigErrorManuallySkipped = unchecked((int)0x8A15C00A); + internal const int WingetConfigErrorWarningNotAccepted = unchecked((int)0x8A15C00B); + internal const int WingetConfigErrorSetDependencyCycle = unchecked((int)0x8A15C00C); + internal const int WingetConfigErrorInvalidFieldValue = unchecked((int)0x8A15C00D); + internal const int WingetConfigErrorMissingField = unchecked((int)0x8A15C00E); + + internal const int WinGetConfigUnitNotFound = unchecked((int)0x8A15C101); + internal const int WinGetConfigUnitNotFoundRepository = unchecked((int)0x8A15C102); + internal const int WinGetConfigUnitMultipleMatches = unchecked((int)0x8A15C103); + internal const int WinGetConfigUnitInvokeGet = unchecked((int)0x8A15C104); + internal const int WinGetConfigUnitInvokeTest = unchecked((int)0x8A15C105); + internal const int WinGetConfigUnitInvokeSet = unchecked((int)0x8A15C106); + internal const int WinGetConfigUnitModuleConflict = unchecked((int)0x8A15C107); + internal const int WinGetConfigUnitImportModule = unchecked((int)0x8A15C108); + internal const int WinGetConfigUnitInvokeInvalidResult = unchecked((int)0x8A15C109); +#pragma warning restore SA1600 // ElementsMustBeDocumented + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ErrorRecordErrorId.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Exceptions +{ + /// <summary> + /// ErrorId used for the ErrorRecords. + /// </summary> + internal enum ErrorRecordErrorId + { + /// <summary> + /// Error message from diagnostics. + /// </summary> + ConfigurationDiagnosticError, + + /// <summary> + /// Error processing details. + /// </summary> + ConfigurationDetailsError, + + /// <summary> + /// Error applying configuration. + /// </summary> + ConfigurationApplyError, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs @@ -0,0 +1,63 @@ +// ----------------------------------------------------------------------------- +// <copyright file="OpenConfigurationSetException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Exceptions +{ + using System; + using System.Text; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Resources; + + /// <summary> + /// Exception thrown when failed to open a configuration set. + /// </summary> + public class OpenConfigurationSetException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="OpenConfigurationSetException"/> class. + /// </summary> + /// <param name="openResult">Open Result.</param> + /// <param name="configurationFile">Configuration file.</param> + public OpenConfigurationSetException(OpenConfigurationSetResult openResult, string configurationFile) + : base(GetMessage(openResult, configurationFile)) + { + } + + private static string GetMessage(OpenConfigurationSetResult openResult, string configurationFile) + { + var sb = new StringBuilder(); + sb.AppendLine($"Failed to open configuration set at {configurationFile} with error 0x{openResult.ResultCode.HResult:X}"); + + switch (openResult.ResultCode.HResult) + { + case ErrorCodes.WingetConfigErrorInvalidFieldType: + sb.AppendLine(string.Format(Resources.ConfigurationFieldInvalidType, openResult.Field)); + break; + case ErrorCodes.WingetConfigErrorInvalidFieldValue: + sb.AppendLine(string.Format(Resources.ConfigurationFieldInvalidValue, openResult.Field, openResult.Value)); + break; + case ErrorCodes.WingetConfigErrorMissingField: + sb.AppendLine(string.Format(Resources.ConfigurationFieldMissing, openResult.Field)); + break; + case ErrorCodes.WingetConfigErrorUnknownConfigurationFileVersion: + sb.AppendLine(string.Format(Resources.ConfigurationFileVersionUnknown, openResult.Value)); + break; + case ErrorCodes.WingetConfigErrorInvalidConfigurationFile: + case ErrorCodes.WingetConfigErrorInvalidYaml: + default: + sb.AppendLine(Resources.ConfigurationFileInvalid); + break; + } + + if (openResult.Line != 0) + { + sb.AppendLine(string.Format(Resources.SeeLineAndColumn, openResult.Line, openResult.Column)); + } + + return sb.ToString(); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/ValueSetExtensions.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/ValueSetExtensions.cs @@ -0,0 +1,33 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ValueSetExtensions.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Extensions +{ + using Windows.Foundation.Collections; + + /// <summary> + /// Extension methods for Value set. + /// </summary> + internal static class ValueSetExtensions + { + /// <summary> + /// Gets the string value of a given key. + /// Null is doesn't exist or cast can't be done. + /// </summary> + /// <param name="valueSet">Value set.</param> + /// <param name="key">Key.</param> + /// <returns>String value.</returns> + public static string? TryGetStringValue(this ValueSet valueSet, string key) + { + if (valueSet.TryGetValue(key, out object value)) + { + return value as string; + } + + return null; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs @@ -0,0 +1,275 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ApplyConfigurationSetProgressOutput.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Helpers +{ + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.Exceptions; + using Microsoft.WinGet.Configuration.Engine.Resources; + using Windows.Foundation; + using static Microsoft.WinGet.Configuration.Engine.Commands.AsyncCommand; + + /// <summary> + /// Helper to handle progress callbacks from ApplyConfigurationSetAsync. + /// </summary> + internal class ApplyConfigurationSetProgressOutput + { + private readonly AsyncCommand cmd; + private readonly int activityId; + private readonly string activity; + private readonly string inProgressMessage; + private readonly string completeMessage; + private readonly int totalUnitsExpected; + + private readonly HashSet<Guid> unitsSeen = new (); + private readonly HashSet<Guid> unitsCompleted = new (); + + private bool isFirstProgress = true; + + /// <summary> + /// Initializes a new instance of the <see cref="ApplyConfigurationSetProgressOutput"/> class. + /// </summary> + /// <param name="cmd">Command that outputs the messages.</param> + /// <param name="activityId">The activity id of the progress bar.</param> + /// <param name="activity">The activity.</param> + /// <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) + { + this.cmd = cmd; + this.activityId = activityId; + this.activity = activity; + this.inProgressMessage = inProgressMessage; + this.completeMessage = completeMessage; + this.totalUnitsExpected = totalUnitsExpected; + + // Write initial progress record. + // For some reason, if this is 0 the progress bar is shown full. Start with 1% + this.cmd.WriteProgressWithPercentage(activityId, activity, $"{this.inProgressMessage} 0/{this.totalUnitsExpected}", 1, 100); + } + + /// <summary> + /// Progress callback. + /// </summary> + /// <param name="operation">Async operation in progress.</param> + /// <param name="data">Change data.</param> + public void Progress(IAsyncOperationWithProgress<ApplyConfigurationSetResult, ConfigurationSetChangeData> operation, ConfigurationSetChangeData data) + { + if (this.isFirstProgress) + { + this.HandleUnreportedProgress(operation.GetResults()); + } + + switch (data.Change) + { + case ConfigurationSetChangeEventType.SetStateChanged: + switch (data.SetState) + { + case ConfigurationSetState.Pending: + this.cmd.Write(StreamType.Information, Utilities.CreateInformationMessage(Resources.ConfigurationWaitingOnAnother)); + break; + } + + break; + case ConfigurationSetChangeEventType.UnitStateChanged: + this.HandleUnitProgress(data.Unit, data.UnitState, data.ResultInformation); + break; + } + } + + /// <summary> + /// Handle unreported progress. + /// </summary> + /// <param name="result">Set result.</param> + public void HandleUnreportedProgress(ApplyConfigurationSetResult result) + { + if (!this.isFirstProgress) + { + this.isFirstProgress = false; + foreach (var unitResult in result.UnitResults) + { + this.HandleUnitProgress(unitResult.Unit, unitResult.State, unitResult.ResultInformation); + } + } + } + + /// <summary> + /// Completes the progress bar. + /// </summary> + public void CompleteProgress() + { + this.cmd.CompleteProgress(this.activityId, this.activity, this.completeMessage); + } + + private void HandleUnitProgress(ConfigurationUnit unit, ConfigurationUnitState state, ConfigurationUnitResultInformation resultInformation) + { + if (this.unitsCompleted.Contains(unit.InstanceIdentifier)) + { + return; + } + + switch (state) + { + case ConfigurationUnitState.Pending: + // The unreported progress handler may send pending units, just ignore them + break; + case ConfigurationUnitState.InProgress: + this.OutputUnitInProgressIfNeeded(unit); + break; + case ConfigurationUnitState.Completed: + this.OutputUnitInProgressIfNeeded(unit); + if (resultInformation.ResultCode == null) + { + this.cmd.Write(StreamType.Information, Utilities.CreateInformationMessage($" {Resources.ConfigurationSuccessfullyApplied}")); + } + else + { + string description = resultInformation.Description.Trim(); + var (message, showDescription) = this.GetUnitFailedMessage(unit, resultInformation); + var sb = new StringBuilder(); + sb.AppendLine($" {message}"); + + if (showDescription && !string.IsNullOrEmpty(description)) + { + bool wasLimited = false; + const int maxLines = 3; + var lines = Utilities.SplitIntoLines(description, maxLines + 1); + + for (int i = 0; i < lines.Length && i < maxLines; i++) + { + sb.AppendLine(lines[i]); + } + + if (lines.Length > maxLines) + { + wasLimited = true; + } + + if (wasLimited || string.IsNullOrEmpty(resultInformation.Details)) + { + sb.AppendLine(Resources.ConfigurationDescriptionWasTruncated); + } + } + + this.cmd.Write(StreamType.Information, Utilities.CreateInformationMessage(sb.ToString(), foregroundColor: ConsoleColor.DarkRed)); + + string errorMessage = $"Configuration unit {unit.UnitName}[{unit.Identifier}] failed with code 0x{resultInformation.ResultCode.HResult:X}" + + $" and error message:\n{description}\n{resultInformation.Details}"; + this.cmd.WriteError( + ErrorRecordErrorId.ConfigurationApplyError, + errorMessage, + resultInformation.ResultCode); + } + + this.CompleteUnit(unit); + break; + case ConfigurationUnitState.Skipped: + this.OutputUnitInProgressIfNeeded(unit); + this.cmd.Write(StreamType.Warning, this.GetUnitSkippedMessage(resultInformation)); + this.CompleteUnit(unit); + break; + } + } + + private void CompleteUnit(ConfigurationUnit unit) + { + if (this.unitsCompleted.Add(unit.InstanceIdentifier)) + { + this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.unitsCompleted.Count}/{this.totalUnitsExpected}", this.unitsCompleted.Count, this.totalUnitsExpected); + } + } + + private void OutputUnitInProgressIfNeeded(ConfigurationUnit unit) + { + var unitInstance = unit.InstanceIdentifier; + if (!this.unitsSeen.Contains(unitInstance)) + { + this.unitsSeen.Add(unitInstance); + var unitInfo = new ConfigurationUnitInformation(unit); + this.cmd.Write(StreamType.Information, unitInfo.GetHeader()); + } + } + + private (string message, bool showDescription) GetUnitFailedMessage(ConfigurationUnit unit, ConfigurationUnitResultInformation resultInformation) + { + if (resultInformation.ResultCode == null) + { + return (string.Format(Resources.ConfigurationUnitFailed, "null"), false); + } + + int resultCode = resultInformation.ResultCode.HResult; + switch (resultCode) + { + case ErrorCodes.WingetConfigErrorDuplicateIdentifier: + return (string.Format(Resources.ConfigurationUnitHasDuplicateIdentifier, unit.Identifier), false); + case ErrorCodes.WingetConfigErrorMissingDependency: + return (string.Format(Resources.ConfigurationUnitHasMissingDependency, resultInformation.Details), false); + case ErrorCodes.WingetConfigErrorAssertionFailed: + return (Resources.ConfigurationUnitAssertHadNegativeResult, false); + case ErrorCodes.WinGetConfigUnitNotFound: + return (Resources.ConfigurationUnitNotFoundInModule, false); + case ErrorCodes.WinGetConfigUnitNotFoundRepository: + return (Resources.ConfigurationUnitNotFound, false); + case ErrorCodes.WinGetConfigUnitMultipleMatches: + return (Resources.ConfigurationUnitMultipleMatches, false); + case ErrorCodes.WinGetConfigUnitInvokeGet: + return (Resources.ConfigurationUnitFailedDuringGet, true); + case ErrorCodes.WinGetConfigUnitInvokeTest: + return (Resources.ConfigurationUnitFailedDuringTest, true); + case ErrorCodes.WinGetConfigUnitInvokeSet: + return (Resources.ConfigurationUnitFailedDuringSet, true); + case ErrorCodes.WinGetConfigUnitModuleConflict: + return (Resources.ConfigurationUnitModuleConflict, false); + case ErrorCodes.WinGetConfigUnitImportModule: + return (Resources.ConfigurationUnitModuleImportFailed, true); + case ErrorCodes.WinGetConfigUnitInvokeInvalidResult: + return (Resources.ConfigurationUnitReturnedInvalidResult, false); + } + + switch (resultInformation.ResultSource) + { + case ConfigurationUnitResultSource.ConfigurationSet: + return (string.Format(Resources.ConfigurationUnitFailedConfigSet, resultCode), true); + case ConfigurationUnitResultSource.Internal: + return (string.Format(Resources.ConfigurationUnitFailedInternal, resultCode), true); + case ConfigurationUnitResultSource.Precondition: + return (string.Format(Resources.ConfigurationUnitFailedPrecondition, resultCode), true); + case ConfigurationUnitResultSource.SystemState: + return (string.Format(Resources.ConfigurationUnitFailedSystemState, resultCode), true); + case ConfigurationUnitResultSource.UnitProcessing: + return (string.Format(Resources.ConfigurationUnitFailedUnitProcessing, resultCode), true); + } + + return (string.Format(Resources.ConfigurationUnitFailed, resultCode), true); + } + + private string GetUnitSkippedMessage(ConfigurationUnitResultInformation resultInformation) + { + if (resultInformation.ResultCode == null) + { + return string.Format(Resources.ConfigurationUnitSkipped, "null"); + } + + int resultCode = resultInformation.ResultCode.HResult; + switch (resultCode) + { + case ErrorCodes.WingetConfigErrorManuallySkipped: + return Resources.ConfigurationUnitManuallySkipped; + case ErrorCodes.WingetConfigErrorDependencyUnsatisfied: + return Resources.ConfigurationUnitNotRunDueToDependency; + case ErrorCodes.WingetConfigErrorAssertionFailed: + return Resources.ConfigurationUnitNotRunDueToFailedAssert; + } + + return string.Format(Resources.ConfigurationUnitSkipped, resultCode); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationUnitInformation.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationUnitInformation.cs @@ -0,0 +1,323 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationUnitInformation.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Helpers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using System.Text; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Extensions; + using Microsoft.WinGet.Configuration.Engine.Resources; + using Windows.Foundation.Collections; + + /// <summary> + /// Helper class to construct the information messages for a unit. + /// This must match or be as close as possible to winget's OutputConfigurationUnitInformation. + /// </summary> + internal class ConfigurationUnitInformation + { + private const string Description = "description"; + private const string Module = "module"; + private const string TreatAsArray = "treatAsArray"; + + private readonly string header; + private readonly string information; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationUnitInformation"/> class. + /// </summary> + /// <param name="unit">Configuration unit.</param> + public ConfigurationUnitInformation(ConfigurationUnit unit) + { + this.header = this.CreateHeader(unit, unit.Details != null ? unit.Details.UnitName : unit.UnitName); + this.information = this.CreateInformation(unit); + } + + /// <summary> + /// Gets the header information message. + /// </summary> + /// <returns>Header information message.</returns> + public HostInformationMessage GetHeader() + { + return Utilities.CreateInformationMessage(this.header, foregroundColor: ConsoleColor.Cyan); + } + + /// <summary> + /// Gets the information message. + /// </summary> + /// <returns>Information message.</returns> + public HostInformationMessage GetInformation() + { + return Utilities.CreateInformationMessage(this.information); + } + + private string CreateHeader(ConfigurationUnit unit, string name) + { + var sb = new StringBuilder(); + sb.Append($"{this.IntentToString(unit.Intent)} :: {name}"); + + string identifier = unit.Identifier; + if (!string.IsNullOrEmpty(identifier)) + { + sb.Append($" [{identifier}]"); + } + + return sb.ToString(); + } + + private string CreateInformation(ConfigurationUnit unit) + { + IConfigurationUnitProcessorDetails details = unit.Details; + ValueSet directives = unit.Directives; + + var sb = new StringBuilder(); + if (details != null) + { + this.CreateInformationWithDetails(ref sb, details, directives); + } + else + { + this.CreateInformationWithoutDetails(ref sb, directives); + } + + // -- Sample output footer -- + // Dependencies: dep1, dep2, ... + // Settings: + // <... settings splat> + var dependencies = unit.Dependencies; + if (dependencies.Count > 0) + { + var dependencySb = new StringBuilder(); + foreach (var dependency in dependencies) + { + dependencySb.Append($" {dependency}"); + } + + sb.AppendLine($" {string.Format(Resources.ConfigurationDependencies, dependencySb.ToString())}"); + } + + var settings = unit.Settings; + if (settings.Count > 0) + { + sb.AppendLine($" {Resources.ConfigurationSettings}"); + this.AppendValueSet(ref sb, settings, 4); + } + + return sb.ToString(); + } + + // -- Sample output when IConfigurationUnitProcessorDetails present -- + // Intent :: UnitName <from details> [Identifier] + // UnitDocumentationUri <if present> + // Description <from details first, directives second> + // "Module": ModuleName "by" Author / Publisher (IsLocal / ModuleSource) + // "Signed by": SigningCertificateChain (leaf subject CN) + // PublishedModuleUri / ModuleDocumentationUri <if present> + // ModuleDescription + private void CreateInformationWithDetails(ref StringBuilder sb, IConfigurationUnitProcessorDetails details, ValueSet directives) + { + var unitDocumentationUri = details.UnitDocumentationUri; + if (unitDocumentationUri != null) + { + sb.AppendLine($" {unitDocumentationUri.AbsoluteUri}"); + } + + var unitDescriptionFromDetails = details.UnitDescription; + if (!string.IsNullOrEmpty(unitDescriptionFromDetails)) + { + sb.AppendLine($" {unitDescriptionFromDetails}"); + } + else + { + var unitDescriptionFromDirectives = directives.TryGetStringValue(Description); + if (!string.IsNullOrEmpty(unitDescriptionFromDirectives)) + { + sb.AppendLine($" {unitDescriptionFromDirectives}"); + } + } + + var author = details.Author; + if (string.IsNullOrEmpty(author)) + { + author = details.Publisher; + } + + if (details.IsLocal) + { + sb.AppendLine($" {string.Format(Resources.ConfigurationModuleWithDetails, details.ModuleName, author, Resources.ConfigurationLocal)}"); + } + else + { + sb.AppendLine($" {string.Format(Resources.ConfigurationModuleWithDetails, details.ModuleName, author, details.ModuleSource)}"); + } + + // TODO: see signature information in ConfigurationFlow.cpp + var moduleUri = details.PublishedModuleUri; + if (moduleUri == null) + { + moduleUri = details.ModuleDocumentationUri; + } + + if (moduleUri != null) + { + sb.AppendLine($" {moduleUri.AbsoluteUri}"); + } + + var moduleDescription = details.ModuleDescription; + if (!string.IsNullOrEmpty(moduleDescription)) + { + sb.AppendLine($" {moduleDescription}"); + } + } + + // -- Sample output when no IConfigurationUnitProcessorDetails present -- + // Intent :: UnitName <from unit> [identifier] + // Description (from directives) + // "Module": module <directive> + private void CreateInformationWithoutDetails(ref StringBuilder sb, ValueSet directives) + { + var unitDescriptionFromDirectives = directives.TryGetStringValue(Description); + if (!string.IsNullOrEmpty(unitDescriptionFromDirectives)) + { + sb.AppendLine($" {unitDescriptionFromDirectives}"); + } + + var unitModuleFromDirectives = directives.TryGetStringValue(Module); + if (!string.IsNullOrEmpty(unitModuleFromDirectives)) + { + sb.AppendLine($" {string.Format(Resources.ConfigurationModuleNameOnly, unitModuleFromDirectives)}"); + } + } + + private void AppendValueSet(ref StringBuilder sb, ValueSet valueSet, int indent) + { + var indentString = new string(' ', indent); + + foreach (var value in valueSet) + { + sb.Append($"{indentString}{value.Key}:"); + + // Can't use IPropertyValue here... + var obj = value.Value; + var innerValueSet = obj as ValueSet; + if (innerValueSet != null) + { + sb.AppendLine(); + if (innerValueSet.ContainsKey(TreatAsArray)) + { + this.AppendValueSetAsArray(ref sb, innerValueSet, indent + 2); + } + else + { + this.AppendValueSet(ref sb, innerValueSet, indent + 2); + } + } + else + { + this.AppendPropertyValue(ref sb, obj); + } + } + } + + private void AppendValueSetAsArray(ref StringBuilder sb, ValueSet valueSet, int indent) + { + var indentString = new string(' ', indent); + + var sortedList = new SortedList<int, object>(); + + foreach (var keyValuePair in valueSet) + { + if (keyValuePair.Key != TreatAsArray) + { + if (int.TryParse(keyValuePair.Key, out int key)) + { + sortedList.Add(key, keyValuePair.Value); + } + else + { + throw new InvalidOperationException(keyValuePair.Key); + } + } + } + + foreach (var arrayValue in sortedList) + { + sb.Append($"{indentString}-"); + var obj = arrayValue.Value; + + var innerValueSet = obj as ValueSet; + if (innerValueSet == null) + { + this.AppendPropertyValue(ref sb, obj); + } + else + { + var size = innerValueSet.Count; + if (size > 0) + { + // First one is special. + var first = innerValueSet.First(); + sb.Append($" {first.Key}:"); + + var firstValueSet = first.Value as ValueSet; + if (firstValueSet == null) + { + this.AppendPropertyValue(ref sb, first.Value); + } + else + { + sb.AppendLine(); + this.AppendValueSet(ref sb, firstValueSet, indent + 4); + } + + if (size > 1) + { + innerValueSet.Remove(first.Key); + this.AppendValueSet(ref sb, innerValueSet, indent + 2); + innerValueSet.Add(first); + } + } + } + } + } + + private void AppendPropertyValue(ref StringBuilder sb, object value) + { + Type type = value.GetType(); + if (type == typeof(string)) + { + sb.AppendLine($" {(string)value}"); + } + else if (type == typeof(bool)) + { + string message = (bool)value ? "true" : "false"; + sb.AppendLine($" {message}"); + } + else if (type == typeof(long)) + { + sb.AppendLine($" {(long)value}"); + } + else + { + sb.AppendLine($" [Debug:PropertyType={type}]"); + } + } + + private string IntentToString(ConfigurationUnitIntent intent) + { + return intent switch + { + ConfigurationUnitIntent.Assert => Resources.ConfigurationAssert, + ConfigurationUnitIntent.Inform => Resources.ConfigurationInform, + ConfigurationUnitIntent.Apply => Resources.ConfigurationApply, + _ => string.Empty, + }; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs @@ -0,0 +1,107 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetConfigurationSetDetailsProgressOutput.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +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.Configuration.Engine.Exceptions; + using Windows.Foundation; + using static Microsoft.WinGet.Configuration.Engine.Commands.AsyncCommand; + + /// <summary> + /// Helper to handle progress callback from GetSetDetailsAsync. + /// </summary> + internal class GetConfigurationSetDetailsProgressOutput + { + private readonly AsyncCommand cmd; + private readonly int activityId; + private readonly string activity; + private readonly string inProgressMessage; + private readonly string completeMessage; + private readonly int totalUnitsExpected; + + /// <summary> + /// Initializes a new instance of the <see cref="GetConfigurationSetDetailsProgressOutput"/> class. + /// </summary> + /// <param name="cmd">Command that outputs the messages.</param> + /// <param name="activityId">The activity id of the progress bar.</param> + /// <param name="activity">The activity.</param> + /// <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) + { + this.cmd = cmd; + this.activityId = activityId; + this.activity = activity; + this.inProgressMessage = inProgressMessage; + this.completeMessage = completeMessage; + this.totalUnitsExpected = totalUnitsExpected; + + // Write initial progress record. + // For some reason, if this is 0 the progress bar is shown full. Start with 1% + this.cmd.WriteProgressWithPercentage(activityId, activity, $"{this.inProgressMessage} 0/{this.totalUnitsExpected}", 1, 100); + } + + /// <summary> + /// Gets the number of units shown. + /// </summary> + internal int UnitsShown { get; private set; } = 0; + + /// <summary> + /// Progress callback. + /// </summary> + /// <param name="operation">Async operation in progress.</param> + /// <param name="result">Result.</param> + public void Progress(IAsyncOperationWithProgress<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult> operation, GetConfigurationUnitDetailsResult result) + { + this.HandleUnits(operation.GetResults().UnitResults); + } + + /// <summary> + /// Handle units. + /// </summary> + /// <param name="unitResults">The unit results.</param> + public void HandleUnits(IReadOnlyList<GetConfigurationUnitDetailsResult> unitResults) + { + while (this.UnitsShown < unitResults.Count) + { + GetConfigurationUnitDetailsResult unitResult = unitResults[this.UnitsShown]; + this.LogFailedGetConfigurationUnitDetails(unitResult.Unit, unitResult.ResultInformation); + var information = new ConfigurationUnitInformation(unitResult.Unit); + this.cmd.Write(StreamType.Information, information.GetHeader()); + this.cmd.Write(StreamType.Information, information.GetInformation()); + + ++this.UnitsShown; + this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.UnitsShown}/{this.totalUnitsExpected}", this.UnitsShown, this.totalUnitsExpected); + } + } + + /// <summary> + /// Complete progress. + /// </summary> + public void CompleteProgress() + { + this.cmd.CompleteProgress(this.activityId, this.activity, this.completeMessage); + } + + private void LogFailedGetConfigurationUnitDetails(ConfigurationUnit unit, ConfigurationUnitResultInformation resultInformation) + { + if (resultInformation.ResultCode != null) + { + string errorMessage = $"Failed to get unit details for {unit.UnitName} 0x{resultInformation.ResultCode.HResult:X}" + + $"{Environment.NewLine}Description: '{resultInformation.Description}'{Environment.NewLine}Details: '{resultInformation.Details}'"; + this.cmd.WriteError( + ErrorRecordErrorId.ConfigurationDetailsError, + errorMessage, + resultInformation.ResultCode); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs @@ -0,0 +1,71 @@ +// ----------------------------------------------------------------------------- +// <copyright file="Utilities.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Helpers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using System.Management.Automation.Host; + + /// <summary> + /// Helper methods. + /// </summary> + internal static class Utilities + { + /// <summary> + /// Helper for StreamType.Information. Creates a HostInformationMessage with + /// the specified information. + /// </summary> + /// <param name="message">Message.</param> + /// <param name="noNewLine">Add not to add a new line.</param> + /// <param name="foregroundColor">Optional foreground color.</param> + /// <param name="backgroundColor">Optional background color.</param> + /// <returns>The information message.</returns> + public static HostInformationMessage CreateInformationMessage( + string message, + bool noNewLine = false, + ConsoleColor? foregroundColor = null, + ConsoleColor? backgroundColor = null) + { + var infoMessage = new HostInformationMessage + { + Message = message, + NoNewLine = noNewLine, + }; + + try + { + infoMessage.ForegroundColor = foregroundColor; + infoMessage.BackgroundColor = backgroundColor; + } + catch (HostException) + { + // Expected if the host is not interactive, or doesn't have Foreground / Background colors. + } + + return infoMessage; + } + + /// <summary> + /// Splits the message into lines. + /// </summary> + /// <param name="message">Message.</param> + /// <param name="maxLines">Max lines.</param> + /// <returns>Lines.</returns> + public static string[] SplitIntoLines(string message, int maxLines) + { + var lines = message.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + if (lines.Length > maxLines) + { + return lines.Take(maxLines).ToArray(); + } + + return lines; + } + } +} 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 @@ -38,4 +38,19 @@ <ProjectReference Include="..\..\Microsoft.Management.Configuration.Processor\Microsoft.Management.Configuration.Processor.csproj" /> </ItemGroup> + <ItemGroup> + <Compile Update="Resources\Resources.Designer.cs"> + <DesignTime>True</DesignTime> + <AutoGen>True</AutoGen> + <DependentUpon>Resources.resx</DependentUpon> + </Compile> + </ItemGroup> + + <ItemGroup> + <EmbeddedResource Update="Resources\Resources.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>Resources.Designer.cs</LastGenOutput> + </EmbeddedResource> + </ItemGroup> + </Project> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationProcessor.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationProcessor.cs @@ -11,6 +11,8 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects 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; /// <summary> /// Creates configuration processor and set up diagnostic logging. @@ -63,29 +65,13 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects { try { - // This is expensive. AsyncCommand asyncCommand = this.diagnosticCommand; - switch (diagnosticInformation.Level) + if (asyncCommand != null) { - // 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; + // 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}"); } } catch (Exception) diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs @@ -0,0 +1,531 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.WinGet.Configuration.Engine.Resources { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.WinGet.Configuration.Engine.Resources.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to Accepts the configuration warning, preventing an interactive prompt. + /// </summary> + internal static string ConfigurationAcceptWarningArgumentDescription { + get { + return ResourceManager.GetString("ConfigurationAcceptWarningArgumentDescription", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Apply. + /// </summary> + internal static string ConfigurationApply { + get { + return ResourceManager.GetString("ConfigurationApply", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Assert. + /// </summary> + internal static string ConfigurationAssert { + get { + return ResourceManager.GetString("ConfigurationAssert", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Dependencies:{0}. + /// </summary> + internal static string ConfigurationDependencies { + get { + return ResourceManager.GetString("ConfigurationDependencies", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to <See the log file for additional details>. + /// </summary> + internal static string ConfigurationDescriptionWasTruncated { + get { + return ResourceManager.GetString("ConfigurationDescriptionWasTruncated", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Some of the configuration was not applied successfully.. + /// </summary> + internal static string ConfigurationFailedToApply { + get { + return ResourceManager.GetString("ConfigurationFailedToApply", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Failed to get detailed information about the configuration.. + /// </summary> + internal static string ConfigurationFailedToGetDetails { + get { + return ResourceManager.GetString("ConfigurationFailedToGetDetails", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The field '{0}' in the configuration file is the wrong type.. + /// </summary> + internal static string ConfigurationFieldInvalidType { + get { + return ResourceManager.GetString("ConfigurationFieldInvalidType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The field '{0}' has an invalid value: {1}. + /// </summary> + internal static string ConfigurationFieldInvalidValue { + get { + return ResourceManager.GetString("ConfigurationFieldInvalidValue", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The field '{0}' is missing or empty.. + /// </summary> + internal static string ConfigurationFieldMissing { + get { + return ResourceManager.GetString("ConfigurationFieldMissing", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The path to the configuration file.. + /// </summary> + internal static string ConfigurationFileArgumentDescription { + get { + return ResourceManager.GetString("ConfigurationFileArgumentDescription", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration is empty.. + /// </summary> + internal static string ConfigurationFileEmpty { + get { + return ResourceManager.GetString("ConfigurationFileEmpty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration file is invalid.. + /// </summary> + internal static string ConfigurationFileInvalid { + get { + return ResourceManager.GetString("ConfigurationFileInvalid", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Configuration file version {0} is not known.. + /// </summary> + internal static string ConfigurationFileVersionUnknown { + get { + return ResourceManager.GetString("ConfigurationFileVersionUnknown", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Retrieving configuration details. + /// </summary> + internal static string ConfigurationGettingDetails { + get { + return ResourceManager.GetString("ConfigurationGettingDetails", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Inform. + /// </summary> + internal static string ConfigurationInform { + get { + return ResourceManager.GetString("ConfigurationInform", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Initializing configuration system. + /// </summary> + internal static string ConfigurationInitializing { + get { + return ResourceManager.GetString("ConfigurationInitializing", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Local. + /// </summary> + internal static string ConfigurationLocal { + get { + return ResourceManager.GetString("ConfigurationLocal", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Module: {0}. + /// </summary> + internal static string ConfigurationModuleNameOnly { + get { + return ResourceManager.GetString("ConfigurationModuleNameOnly", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Module: {0} by {1} [{2}]. + /// </summary> + internal static string ConfigurationModuleWithDetails { + get { + return ResourceManager.GetString("ConfigurationModuleWithDetails", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Reading configuration file. + /// </summary> + internal static string ConfigurationReadingConfigFile { + get { + return ResourceManager.GetString("ConfigurationReadingConfigFile", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Settings:. + /// </summary> + internal static string ConfigurationSettings { + get { + return ResourceManager.GetString("ConfigurationSettings", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Configuration successfully applied.. + /// </summary> + internal static string ConfigurationSuccessfullyApplied { + get { + return ResourceManager.GetString("ConfigurationSuccessfullyApplied", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The system is not in the desired state asserted by the configuration.. + /// </summary> + internal static string ConfigurationUnitAssertHadNegativeResult { + get { + return ResourceManager.GetString("ConfigurationUnitAssertHadNegativeResult", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This configuration unit failed for an unknown reason: {0}. + /// </summary> + internal static string ConfigurationUnitFailed { + get { + return ResourceManager.GetString("ConfigurationUnitFailed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit failed due to the configuration: {0}. + /// </summary> + internal static string ConfigurationUnitFailedConfigSet { + get { + return ResourceManager.GetString("ConfigurationUnitFailedConfigSet", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit failed while attempting to get the current system state.. + /// </summary> + internal static string ConfigurationUnitFailedDuringGet { + get { + return ResourceManager.GetString("ConfigurationUnitFailedDuringGet", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit failed while attempting to apply the desired state.. + /// </summary> + internal static string ConfigurationUnitFailedDuringSet { + get { + return ResourceManager.GetString("ConfigurationUnitFailedDuringSet", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit failed while attempting to test the current system state.. + /// </summary> + internal static string ConfigurationUnitFailedDuringTest { + get { + return ResourceManager.GetString("ConfigurationUnitFailedDuringTest", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit failed due to an internal error: {0}. + /// </summary> + internal static string ConfigurationUnitFailedInternal { + get { + return ResourceManager.GetString("ConfigurationUnitFailedInternal", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit failed due to a precondition not being valid: {0}. + /// </summary> + internal static string ConfigurationUnitFailedPrecondition { + get { + return ResourceManager.GetString("ConfigurationUnitFailedPrecondition", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit failed due to the system state: {0}. + /// </summary> + internal static string ConfigurationUnitFailedSystemState { + get { + return ResourceManager.GetString("ConfigurationUnitFailedSystemState", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit failed while attempting to run: {0}. + /// </summary> + internal static string ConfigurationUnitFailedUnitProcessing { + get { + return ResourceManager.GetString("ConfigurationUnitFailedUnitProcessing", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration contains the identifier `{0}` multiple times.. + /// </summary> + internal static string ConfigurationUnitHasDuplicateIdentifier { + get { + return ResourceManager.GetString("ConfigurationUnitHasDuplicateIdentifier", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The dependency `{0}` was not found within the configuration.. + /// </summary> + internal static string ConfigurationUnitHasMissingDependency { + get { + return ResourceManager.GetString("ConfigurationUnitHasMissingDependency", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This configuration unit was manually skipped.. + /// </summary> + internal static string ConfigurationUnitManuallySkipped { + get { + return ResourceManager.GetString("ConfigurationUnitManuallySkipped", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The module for the configuration unit is available in multiple locations with the same version.. + /// </summary> + internal static string ConfigurationUnitModuleConflict { + get { + return ResourceManager.GetString("ConfigurationUnitModuleConflict", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Loading the module for the configuration unit failed.. + /// </summary> + internal static string ConfigurationUnitModuleImportFailed { + get { + return ResourceManager.GetString("ConfigurationUnitModuleImportFailed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Multiple matches were found for the configuration unit; specify the module to select the correct one.. + /// </summary> + internal static string ConfigurationUnitMultipleMatches { + get { + return ResourceManager.GetString("ConfigurationUnitMultipleMatches", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit could not be found.. + /// </summary> + internal static string ConfigurationUnitNotFound { + get { + return ResourceManager.GetString("ConfigurationUnitNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit was not in the module as expected.. + /// </summary> + internal static string ConfigurationUnitNotFoundInModule { + get { + return ResourceManager.GetString("ConfigurationUnitNotFoundInModule", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This configuration unit was not run because a dependency failed or was not run.. + /// </summary> + internal static string ConfigurationUnitNotRunDueToDependency { + get { + return ResourceManager.GetString("ConfigurationUnitNotRunDueToDependency", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This configuration unit was not run because an assert failed or was false.. + /// </summary> + internal static string ConfigurationUnitNotRunDueToFailedAssert { + get { + return ResourceManager.GetString("ConfigurationUnitNotRunDueToFailedAssert", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration unit returned an unexpected result during execution.. + /// </summary> + internal static string ConfigurationUnitReturnedInvalidResult { + get { + return ResourceManager.GetString("ConfigurationUnitReturnedInvalidResult", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This configuration unit was not run for an unknown reason: {0}. + /// </summary> + internal static string ConfigurationUnitSkipped { + get { + return ResourceManager.GetString("ConfigurationUnitSkipped", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Another configuration is being applied to the system. This configuration will continue as soon as is possible.... + /// </summary> + internal static string ConfigurationWaitingOnAnother { + get { + return ResourceManager.GetString("ConfigurationWaitingOnAnother", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to You are responsible for understanding the configuration settings you are choosing to execute. Microsoft is not responsible for the configuration file you have authored or imported. This configuration may change settings in Windows, install software, change software settings (including security settings), and accept user agreements to third-party packages and services on your behalf. By running this configuration file, you acknowledge that you understand and agree to these resources and settings. Any applic [rest of string was truncated]";. + /// </summary> + internal static string ConfigurationWarning { + get { + return ResourceManager.GetString("ConfigurationWarning", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Have you reviewed the configuration and would you like to proceed applying it to the system?. + /// </summary> + internal static string ConfigurationWarningPrompt { + get { + return ResourceManager.GetString("ConfigurationWarningPrompt", resourceCulture); + } + } + + /// <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 Completed. + /// </summary> + internal static string OperationCompleted { + get { + return ResourceManager.GetString("OperationCompleted", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to In progress. + /// </summary> + internal static string OperationInProgress { + get { + return ResourceManager.GetString("OperationInProgress", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to See line {0}, column {1} in the file.. + /// </summary> + internal static string SeeLineAndColumn { + get { + return ResourceManager.GetString("SeeLineAndColumn", resourceCulture); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx @@ -0,0 +1,301 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="ConfigurationFieldInvalidType" xml:space="preserve"> + <value>The field '{0}' in the configuration file is the wrong type.</value> + <comment>{Locked="{0}"} An error in reading a configuration file. {0} is a placeholder replaced by the field name from the file.</comment> + </data> + <data name="ConfigurationFileArgumentDescription" xml:space="preserve"> + <value>The path to the configuration file.</value> + </data> + <data name="ConfigurationFileInvalid" xml:space="preserve"> + <value>The configuration file is invalid.</value> + </data> + <data name="ConfigurationFileVersionUnknown" xml:space="preserve"> + <value>Configuration file version {0} is not known.</value> + <comment>{Locked="{0}"} An error in reading a configuration file. {0} is a placeholder replaced by the version of the configuration file.</comment> + </data> + <data name="ConfigurationAcceptWarningArgumentDescription" xml:space="preserve"> + <value>Accepts the configuration warning, preventing an interactive prompt</value> + </data> + <data name="ConfigurationApply" xml:space="preserve"> + <value>Apply</value> + <comment>Indicates that this item is used to write state</comment> + </data> + <data name="ConfigurationAssert" xml:space="preserve"> + <value>Assert</value> + <comment>Indicates that this item is used to check/assert the state rather than write to it</comment> + </data> + <data name="ConfigurationDependencies" xml:space="preserve"> + <value>Dependencies:{0}</value> + <comment>{Locked="{0}"} Label displaying a list of dependencies. {0} is replaced with a space separated list of identifiers referencing other items.</comment> + </data> + <data name="ConfigurationFailedToApply" xml:space="preserve"> + <value>Some of the configuration was not applied successfully.</value> + </data> + <data name="ConfigurationFailedToGetDetails" xml:space="preserve"> + <value>Failed to get detailed information about the configuration.</value> + </data> + <data name="ConfigurationInform" xml:space="preserve"> + <value>Inform</value> + <comment>Indicates that this item is used to retrieve values for future use rather than writing them</comment> + </data> + <data name="ConfigurationLocal" xml:space="preserve"> + <value>Local</value> + <comment>Used to indicate that the item is present on the device.</comment> + </data> + <data name="ConfigurationModuleNameOnly" xml:space="preserve"> + <value>Module: {0}</value> + <comment>{Locked="{0}"} Label displaying a module name. {0} is replaced with the name of the module from the user input file.</comment> + </data> + <data name="ConfigurationModuleWithDetails" xml:space="preserve"> + <value>Module: {0} by {1} [{2}]</value> + <comment>{Locked="{0}","{1}","{2}"} Label displaying module information. {0} is replaced by the module name. {1} is replaced by the module author. {2} is replaced by a string indicating the source of the module.</comment> + </data> + <data name="ConfigurationSettings" xml:space="preserve"> + <value>Settings:</value> + <comment>Label for the values that are used as inputs for this item when applying state</comment> + </data> + <data name="ConfigurationSuccessfullyApplied" xml:space="preserve"> + <value>Configuration successfully applied.</value> + </data> + <data name="ConfigurationWaitingOnAnother" xml:space="preserve"> + <value>Another configuration is being applied to the system. This configuration will continue as soon as is possible...</value> + </data> + <data name="ConfigurationWarning" xml:space="preserve"> + <value>You are responsible for understanding the configuration settings you are choosing to execute. Microsoft is not responsible for the configuration file you have authored or imported. This configuration may change settings in Windows, install software, change software settings (including security settings), and accept user agreements to third-party packages and services on your behalf. By running this configuration file, you acknowledge that you understand and agree to these resources and settings. Any applications installed are licensed to you by their owners. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages or services.</value> + <comment>Legal approved. Do not change without approval.</comment> + </data> + <data name="ConfigurationWarningPrompt" xml:space="preserve"> + <value>Have you reviewed the configuration and would you like to proceed applying it to the system?</value> + <comment>PM approved.</comment> + </data> + <data name="ConfigurationFileEmpty" xml:space="preserve"> + <value>The configuration is empty.</value> + </data> + <data name="ConfigurationDescriptionWasTruncated" xml:space="preserve"> + <value><See the log file for additional details></value> + <comment>The brackets are intended to make the value stand out from other text which it will follow. Any locale appropriate mechanism that achieves this is acceptable.</comment> + </data> + <data name="ConfigurationGettingDetails" xml:space="preserve"> + <value>Retrieving configuration details</value> + </data> + <data name="ConfigurationInitializing" xml:space="preserve"> + <value>Initializing configuration system</value> + </data> + <data name="ConfigurationReadingConfigFile" xml:space="preserve"> + <value>Reading configuration file</value> + </data> + <data name="ConfigurationUnitAssertHadNegativeResult" xml:space="preserve"> + <value>The system is not in the desired state asserted by the configuration.</value> + </data> + <data name="ConfigurationUnitFailed" xml:space="preserve"> + <value>This configuration unit failed for an unknown reason: {0}</value> + <comment>{Locked="{0}"} {0} is a placeholder for the unrecognized error code.</comment> + </data> + <data name="ConfigurationUnitFailedConfigSet" xml:space="preserve"> + <value>The configuration unit failed due to the configuration: {0}</value> + <comment>{Locked="{0}"} {0} is a placeholder for the unrecognized error code.</comment> + </data> + <data name="ConfigurationUnitFailedDuringGet" xml:space="preserve"> + <value>The configuration unit failed while attempting to get the current system state.</value> + </data> + <data name="ConfigurationUnitFailedDuringSet" xml:space="preserve"> + <value>The configuration unit failed while attempting to apply the desired state.</value> + </data> + <data name="ConfigurationUnitFailedDuringTest" xml:space="preserve"> + <value>The configuration unit failed while attempting to test the current system state.</value> + </data> + <data name="ConfigurationUnitFailedInternal" xml:space="preserve"> + <value>The configuration unit failed due to an internal error: {0}</value> + <comment>{Locked="{0}"} {0} is a placeholder for the unrecognized error code.</comment> + </data> + <data name="ConfigurationUnitFailedPrecondition" xml:space="preserve"> + <value>The configuration unit failed due to a precondition not being valid: {0}</value> + <comment>{Locked="{0}"} {0} is a placeholder for the unrecognized error code.</comment> + </data> + <data name="ConfigurationUnitFailedSystemState" xml:space="preserve"> + <value>The configuration unit failed due to the system state: {0}</value> + <comment>{Locked="{0}"} {0} is a placeholder for the unrecognized error code.</comment> + </data> + <data name="ConfigurationUnitFailedUnitProcessing" xml:space="preserve"> + <value>The configuration unit failed while attempting to run: {0}</value> + <comment>{Locked="{0}"} {0} is a placeholder for the unrecognized error code.</comment> + </data> + <data name="ConfigurationUnitHasDuplicateIdentifier" xml:space="preserve"> + <value>The configuration contains the identifier `{0}` multiple times.</value> + <comment>{Locked="{0}"} {0} is a placeholder that is replaced by the identifier string from the user input file.</comment> + </data> + <data name="ConfigurationUnitHasMissingDependency" xml:space="preserve"> + <value>The dependency `{0}` was not found within the configuration.</value> + <comment>{Locked="{0}"} {0} is a placeholder that is replaced by the identifier string from the user input file.</comment> + </data> + <data name="ConfigurationUnitManuallySkipped" xml:space="preserve"> + <value>This configuration unit was manually skipped.</value> + </data> + <data name="ConfigurationUnitModuleConflict" xml:space="preserve"> + <value>The module for the configuration unit is available in multiple locations with the same version.</value> + </data> + <data name="ConfigurationUnitModuleImportFailed" xml:space="preserve"> + <value>Loading the module for the configuration unit failed.</value> + </data> + <data name="ConfigurationUnitMultipleMatches" xml:space="preserve"> + <value>Multiple matches were found for the configuration unit; specify the module to select the correct one.</value> + </data> + <data name="ConfigurationUnitNotFound" xml:space="preserve"> + <value>The configuration unit could not be found.</value> + </data> + <data name="ConfigurationUnitNotFoundInModule" xml:space="preserve"> + <value>The configuration unit was not in the module as expected.</value> + </data> + <data name="ConfigurationUnitNotRunDueToDependency" xml:space="preserve"> + <value>This configuration unit was not run because a dependency failed or was not run.</value> + </data> + <data name="ConfigurationUnitNotRunDueToFailedAssert" xml:space="preserve"> + <value>This configuration unit was not run because an assert failed or was false.</value> + </data> + <data name="ConfigurationUnitReturnedInvalidResult" xml:space="preserve"> + <value>The configuration unit returned an unexpected result during execution.</value> + </data> + <data name="ConfigurationUnitSkipped" xml:space="preserve"> + <value>This configuration unit was not run for an unknown reason: {0}</value> + <comment>{Locked="{0}"} {0} is a placeholder for the unrecognized error code.</comment> + </data> + <data name="ConfigurationFieldInvalidValue" xml:space="preserve"> + <value>The field '{0}' has an invalid value: {1}</value> + <comment>{Locked="{0}","{1}"} An error in reading a configuration file. {0} is a placeholder replaced by the field name from the file. {1} is a placeholder for the invalid value.</comment> + </data> + <data name="ConfigurationFieldMissing" xml:space="preserve"> + <value>The field '{0}' is missing or empty.</value> + <comment>{Locked="{0}"} An error in reading a configuration file. {0} is a placeholder replaced by the expected field name from the file.</comment> + </data> + <data name="SeeLineAndColumn" xml:space="preserve"> + <value>See line {0}, column {1} in the file.</value> + <comment>{Locked="{0}","{1}"} Indicates the file location of the error, {0} and {1} are placeholders for numbers of the line and column, respectively.</comment> + </data> + <data name="OperationCompleted" xml:space="preserve"> + <value>Completed</value> + </data> + <data name="OperationInProgress" xml:space="preserve"> + <value>In progress</value> + </data> + <data name="DebugNotSupported" xml:space="preserve"> + <value>Debug parameter not supported</value> + </data> +</root>+ \ No newline at end of file