commit c344db7c0432102fd7a082af4a015b5862b4d706 parent f255b6e1f6e3022ca8521941e052e187781dd84d Author: Ruben Guerrero <rubengu@microsoft.com> Date: Tue, 2 May 2023 14:13:24 -0700 Initial implementation of Microsoft.WinGet.Configuration cmdlets. (#3204) This PR adds 5 cmdlets Get-WinGetConfiguration: creates a configuration set given a file path. Get-WinGetConfigurationDetails: gets the details from a set. Invoke-WinGetConfiguration: applies the configuration and waits for completion. Start-WinGetConfiguration: starts applying the configuration asynchronously. Returns PSConfigurationTask. Complete-WinGetConfiguration: waits for the PSConfigurationTask to be completed. For now, a call the Get-WinGetConfiguration is required to start. It returns a PSConfigurationSet which can be pass to the other cmdlets. If Invoke-WinGetConfiguration or Start-WinGetConfiguration get called before Get-WinGetConfigurationDetails, they will retrieve the details. There is no output in PowerShell except for debug messages and the objects. Future PRs will provide a better user experience similar to what winget configure does. By default, Start-WinGetConfiguration won't write anything to the stream buffers (except the returned object).because it needs to be executed in the main thread. When Complete-WinGetConfiguration gets called, writting to the streams gets enabled. For now, all the messages before Complete gets lost but in the future, we can store them and show them (similar to what Start-Job/Receive-Job does). This PR also adds a new ConfigurationProcessorPolicy enum for creating a ConfigurationSetProcessor and can be added via IConfigurationProcessorFactoryProperties. The enum is a mirror of the PowerShell Execution Policies. By default, winget uses Unrestricted (we should change in the near future) and the module uses the same as the current PowerShell session. I couldn't get the execution policy at the time of creating the set processor because it is not possible to create a runspace based on the current session. At that point, we are already running in another thread so the runspace can't be found. This means that the two types of runspaces are always "hosted" which just means that there's a new runspace created from them. When we enable variables in configuration, there needs to be something that makes them visible from the current PowerShell session to our runspace (maybe using synchronized hashtable). Diffstat:
22 files changed, 1093 insertions(+), 291 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -3,6 +3,7 @@ accepteula adjacents adml admx +AFAIK agg aicli AICLIC diff --git a/src/ConfigurationRemotingServer/Program.cs b/src/ConfigurationRemotingServer/Program.cs @@ -26,6 +26,9 @@ namespace ConfigurationRemotingServer ConfigurationProcessorFactoryProperties properties = new ConfigurationProcessorFactoryProperties(); properties.AdditionalModulePaths = new List<string>() { modulesPath }; + // This can be RemoteSigned eventually or keep it Unrestricted for dev builds. + properties.Policy = ConfigurationProcessorPolicy.Unrestricted; + ConfigurationSetProcessorFactory factory = new ConfigurationSetProcessorFactory(ConfigurationProcessorType.Hosted, properties); IObjectReference factoryInterface = MarshalInterface<global::Microsoft.Management.Configuration.IConfigurationSetProcessorFactory>.CreateMarshaler(factory); diff --git a/src/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs b/src/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs @@ -17,6 +17,7 @@ namespace Microsoft.Management.Configuration.Processor.Constants internal static class Variables { public const string PSEdition = "PSEdition"; + public const string Error = "Error"; public const string PSModulePath = "env:PSModulePath"; } diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs @@ -7,6 +7,7 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces { using System; + using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -29,7 +30,7 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces /// </summary> internal class HostedEnvironment : IProcessorEnvironment { - private ConfigurationProcessorType type; + private readonly ConfigurationProcessorType type; /// <summary> /// Initializes a new instance of the <see cref="HostedEnvironment"/> class. @@ -66,6 +67,17 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces throw new NotSupportedException("Only PowerShell Core is supported."); } + // If opening a runspace has failures, like one of the modules in ImportPSModule is not found, it won't throw but + // write to the error output. This is not a fatal error, since we install PSDesiredStateConfiguration + // module if not found, so unless there's a real reason keep it in verbose. + var errors = this.GetVariable<ArrayList>(Variables.Error); + if (errors.Count > 0) + { + this.OnDiagnostics( + DiagnosticLevel.Verbose, + $"Error creating runspace '{string.Join("\n", errors.Cast<string>().ToArray())}'"); + } + var powerShellGet = PowerShellHelpers.CreateModuleSpecification( Modules.PowerShellGet, minVersion: Modules.PowerShellGetMinVersion); @@ -478,5 +490,10 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces { this.SetProcessorFactory?.OnDiagnostics(level, pwsh); } + + private void OnDiagnostics(DiagnosticLevel level, string message) + { + this.SetProcessorFactory?.OnDiagnostics(level, message); + } } } diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/ProcessorEnvironmentFactory.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/ProcessorEnvironmentFactory.cs @@ -34,32 +34,40 @@ namespace Microsoft.Management.Configuration.Processor.ProcessorEnvironments /// Create process environment. /// </summary> /// <param name="setProcessorFactory">Optional processor factory.</param> + /// <param name="policy">Configuration processor policy.</param> /// <returns>IProcessorEnvironment.</returns> - public IProcessorEnvironment CreateEnvironment(ConfigurationSetProcessorFactory? setProcessorFactory) + public IProcessorEnvironment CreateEnvironment( + ConfigurationSetProcessorFactory? setProcessorFactory, + ConfigurationProcessorPolicy policy) { IDscModule dscModule = new DscModuleV2(); + ExecutionPolicy executionPolicy = this.GetExecutionPolicy(policy); - if (this.type == ConfigurationProcessorType.Default) + // The for ConfigurationProcessorType.Default the idea was that since is already running in PowerShell we will + // have access to the variables in the current runspace, but we can't use that runspace and AFAIK + // there's not a simple way to simply clone a runspace. If we want to do it, we will need to get the + // variables from the current runspace and add them here, but maybe some of them are objects that can't + // handle being used in different runspace. It will also be time consuming and we can't block for creating + // the create set processor. Even if we could clone it, at this point we are running in a different thread, + // so there's no default runspace to clone here (aka. PowerShell.Create(RunspaceMode.CurrentRunspace) throws) + // + // If we want to somehow support, it might be easier to explicitly ask for the variables that need to be + // ported. We can add a new property to IConfigurationProcessorFactoryProperties with the variable names + // and set them here, but if they change they won't get reflected in our runspace (which might be a good thing). + // The problem with that is that they will need to be defined when the configuration set is opened and it really + // just makes sense before the ConfigurationSetProcessor gets created. We could add a new IConfigurationSetProcessorProperties + // Then in PowerShell it can be something like + // Get-WinGetConfiguration | Add-WinGetConfigurationVariable -Name foo | Start-WinGetConfiguration + if (this.type == ConfigurationProcessorType.Hosted || + this.type == ConfigurationProcessorType.Default) { - throw new NotImplementedException(); - } - else if (this.type == ConfigurationProcessorType.Hosted) - { - InitialSessionState initialSessionState = InitialSessionState.CreateDefault(); - - // If this call fails importing the module, it won't throw but write to the error output. DSCModule is - // in charge of verifying that it got loaded correctly and if not, to install it. Once logging is implemented - // we should log the Error PSVariable. - initialSessionState.ImportPSModule(new List<ModuleSpecification>() - { - dscModule.ModuleSpecification, - }); + var initialSessionState = this.CreateInitialSessionState( + executionPolicy, + new List<ModuleSpecification> + { + dscModule.ModuleSpecification, + }); - // This is where our policy will get translated to PowerShell's execution policy. - initialSessionState.ExecutionPolicy = ExecutionPolicy.Unrestricted; - - // The $PSHome\Modules directory is added by default in the modules path. Because this is a hosted PowerShell, - // we don't have all the nice things that PowerShell installs by default. This includes PowerShellGet. var runspace = RunspaceFactory.CreateRunspace(initialSessionState); runspace.Open(); @@ -71,5 +79,31 @@ namespace Microsoft.Management.Configuration.Processor.ProcessorEnvironments throw new ArgumentException(this.type.ToString()); } + + private InitialSessionState CreateInitialSessionState(ExecutionPolicy policy, IReadOnlyList<ModuleSpecification> modules) + { + InitialSessionState initialSessionState = InitialSessionState.CreateDefault(); + + // If this call fails importing the module, it won't throw but write to the error output. DSCModule is + // in charge of verifying that it got loaded correctly and if not, to install it. + initialSessionState.ImportPSModule(modules); + + initialSessionState.ExecutionPolicy = policy; + + return initialSessionState; + } + + private ExecutionPolicy GetExecutionPolicy(ConfigurationProcessorPolicy policy) + { + return policy switch + { + ConfigurationProcessorPolicy.Unrestricted => ExecutionPolicy.Unrestricted, + ConfigurationProcessorPolicy.RemoteSigned => ExecutionPolicy.RemoteSigned, + ConfigurationProcessorPolicy.AllSigned => ExecutionPolicy.AllSigned, + ConfigurationProcessorPolicy.Restricted => ExecutionPolicy.Restricted, + ConfigurationProcessorPolicy.Bypass => ExecutionPolicy.Bypass, + _ => throw new InvalidOperationException(), + }; + } } } diff --git a/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationProcessorFactoryProperties.cs b/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationProcessorFactoryProperties.cs @@ -22,5 +22,8 @@ namespace Microsoft.Management.Configuration.Processor /// <inheritdoc/> public IReadOnlyList<string>? AdditionalModulePaths { get; set; } + + /// <inheritdoc/> + public ConfigurationProcessorPolicy Policy { get; set; } = ConfigurationProcessorPolicy.Default; } } diff --git a/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationProcessorPolicy.cs b/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationProcessorPolicy.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationProcessorPolicy.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor +{ + /// <summary> + /// Processor policy. + /// For Processor type Default and Hosted they mean the same as PowerShell ExecutionPolicy. + /// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies. + /// </summary> + public enum ConfigurationProcessorPolicy + { + /// <summary> + /// Unrestricted. + /// </summary> + Unrestricted = 0, + + /// <summary> + /// RemoteSigned. + /// </summary> + RemoteSigned = 1, + + /// <summary> + /// AllSigned. + /// </summary> + AllSigned = 2, + + /// <summary> + /// Restricted. + /// </summary> + Restricted = 3, + + /// <summary> + /// Bypass. + /// </summary> + Bypass = 4, + + /// <summary> + /// Undefined. + /// </summary> + Undefined = 5, + + /// <summary> + /// Default. + /// </summary> + Default = Restricted, + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationSetProcessorFactory.cs b/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationSetProcessorFactory.cs @@ -55,7 +55,9 @@ namespace Microsoft.Management.Configuration.Processor this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating set processor for `{set.Name}`..."); var envFactory = new ProcessorEnvironmentFactory(this.type); - var processorEnvironment = envFactory.CreateEnvironment(this); + var processorEnvironment = envFactory.CreateEnvironment( + this, + this.properties?.Policy ?? ConfigurationProcessorPolicy.RemoteSigned); if (this.properties is not null) { diff --git a/src/Microsoft.Management.Configuration.Processor/Public/IConfigurationProcessorFactoryProperties.cs b/src/Microsoft.Management.Configuration.Processor/Public/IConfigurationProcessorFactoryProperties.cs @@ -17,5 +17,10 @@ namespace Microsoft.Management.Configuration.Processor /// Gets or sets the additional module paths. /// </summary> IReadOnlyList<string>? AdditionalModulePaths { get; set; } + + /// <summary> + /// Gets or sets the configuration policy. + /// </summary> + ConfigurationProcessorPolicy Policy { get; set; } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Fixtures/UnitTestFixture.cs b/src/Microsoft.Management.Configuration.UnitTests/Fixtures/UnitTestFixture.cs @@ -87,7 +87,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures /// <returns>PowerShellRunspace.</returns> internal IProcessorEnvironment PrepareTestProcessorEnvironment(bool validate = false) { - var processorEnv = new ProcessorEnvironmentFactory(ConfigurationProcessorType.Hosted).CreateEnvironment(null); + var processorEnv = new ProcessorEnvironmentFactory(ConfigurationProcessorType.Hosted).CreateEnvironment(null, ConfigurationProcessorPolicy.Unrestricted); processorEnv.PrependPSModulePath(this.ExternalModulesPath); processorEnv.PrependPSModulePath(this.TestModulesPath); diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs @@ -0,0 +1,42 @@ +// ----------------------------------------------------------------------------- +// <copyright file="CompleteWinGetConfigurationCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using System.Threading; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// <summary> + /// Complete-WinGetConfiguration. + /// Completes a configuration previously started by Start-WinGetConfiguration. + /// Waits for completion. + /// </summary> + [Cmdlet(VerbsLifecycle.Complete, "WinGetConfiguration")] + public sealed class CompleteWinGetConfigurationCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the configuration task. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationJob ConfigurationJob { get; set; } + + /// <summary> + /// Starts to apply the configuration and wait for it to complete. + /// </summary> + protected override void ProcessRecord() + { + CancellationTokenSource source = new (); + + var configCommand = new ConfigurationCommand(this, source.Token); + configCommand.Continue(this.ConfigurationJob); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetWinGetConfigurationCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +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; + + /// <summary> + /// Get-WinGetConfiguration. + /// Opens a configuration set. + /// </summary> + [Cmdlet(VerbsCommon.Get, "WinGetConfiguration")] + public sealed class GetWinGetConfigurationCmdlet : PSCmdlet + { + private ExecutionPolicy executionPolicy = ExecutionPolicy.Undefined; + + /// <summary> + /// Gets or sets the configuration file. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipelineByPropertyName = true)] + public string File { get; set; } + + /// <summary> + /// Pre-processing operations. + /// </summary> + protected override void BeginProcessing() + { + this.executionPolicy = Utilities.GetExecutionPolicy(); + } + + /// <summary> + /// Opens the configuration set. + /// </summary> + protected override void ProcessRecord() + { + CancellationTokenSource source = new (); + + var configCommand = new ConfigurationCommand(this, source.Token); + configCommand.Get(this.File, this.executionPolicy); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetWinGetConfigurationDetailsCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using System.Threading; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// <summary> + /// Get-WinGetConfigurationDetails. + /// Gets the details for the units in a configuration set. + /// </summary> + [Cmdlet(VerbsCommon.Get, "WinGetConfigurationDetails")] + public sealed class GetWinGetConfigurationDetailsCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the configuration set. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// <summary> + /// Starts configuration and wait for it to complete. + /// </summary> + protected override void ProcessRecord() + { + CancellationTokenSource source = new (); + + var configCommand = new ConfigurationCommand(this, source.Token); + configCommand.GetDetails(this.Set); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs @@ -9,42 +9,47 @@ namespace Microsoft.WinGet.Configuration.Cmdlets using System.Management.Automation; using System.Threading; using Microsoft.WinGet.Configuration.Engine.Commands; - using Microsoft.WinGet.Configuration.Helpers; + using Microsoft.WinGet.Configuration.Engine.PSObjects; /// <summary> /// Invoke-WinGetConfiguration. - /// Start configuration and waits for completion. + /// Applies the configuration. + /// Wait for completion. /// </summary> [Cmdlet(VerbsLifecycle.Invoke, "WinGetConfiguration")] public sealed class InvokeWinGetConfigurationCmdlet : PSCmdlet { /// <summary> - /// Gets or sets the configuration file. + /// Gets or sets the configuration set. /// </summary> [Parameter( Mandatory = true, + ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] - public string File { get; set; } + public PSConfigurationSet Set { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to accept the configuration agreements. + /// </summary> + public SwitchParameter AcceptConfigurationAgreements { get; set; } /// <summary> /// Pre-processing operations. /// </summary> protected override void BeginProcessing() { - // The cmdlet doesn't inherit the location from the current session. - // Change it to support relative paths. - Utilities.ChangeToCurrentSessionLocation(); + // TODO: if not agrementsAccepted print message with ShouldContinue. } /// <summary> - /// Starts configuration and wait for it to complete. + /// Starts to apply the configuration and wait for it to complete. /// </summary> protected override void ProcessRecord() { CancellationTokenSource source = new (); - var configCommand = new ConfigurationCommand(this, source.Token, this.File); - configCommand.Invoke(); + var configCommand = new ConfigurationCommand(this, source.Token, false); + 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 @@ -0,0 +1,55 @@ +// ----------------------------------------------------------------------------- +// <copyright file="StartWinGetConfigurationCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using System.Threading; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// <summary> + /// Start-WinGetConfiguration. + /// Start to apply the configuration. + /// Does not wait for completion. + /// </summary> + [Cmdlet(VerbsLifecycle.Start, "WinGetConfiguration")] + public sealed class StartWinGetConfigurationCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the configuration set. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to accept the configuration agreements. + /// </summary> + public SwitchParameter AcceptConfigurationAgreements { get; set; } + + /// <summary> + /// Pre-processing operations. + /// </summary> + protected override void BeginProcessing() + { + // TODO: if not agrementsAccepted print message with ShouldContinue. + } + + /// <summary> + /// Starts to apply the configuration and wait for it to complete. + /// </summary> + protected override void ProcessRecord() + { + CancellationTokenSource source = new (); + + var configCommand = new ConfigurationCommand(this, source.Token, false); + configCommand.StartApply(this.Set); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Utilities.cs @@ -6,10 +6,9 @@ namespace Microsoft.WinGet.Configuration.Helpers { - using System; - using System.IO; using System.Linq; using System.Management.Automation; + using Microsoft.PowerShell; /// <summary> /// Utilities for this cmdlets. @@ -17,19 +16,13 @@ namespace Microsoft.WinGet.Configuration.Helpers internal static class Utilities { /// <summary> - /// Change the current session to the location where the cmdlet - /// got executed. + /// Gets the execution policy. /// </summary> - public static void ChangeToCurrentSessionLocation() + /// <returns>ExecutionPolicy.</returns> + public static ExecutionPolicy GetExecutionPolicy() { var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); - var results = ps.AddCommand("Get-Location").Invoke<PathInfo>(); - if (results is null || results.Count == 0) - { - throw new InvalidOperationException(); - } - - Directory.SetCurrentDirectory(results.First().Path); + return ps.AddCommand("Get-ExecutionPolicy").Invoke<ExecutionPolicy>().First(); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs @@ -0,0 +1,327 @@ +// ----------------------------------------------------------------------------- +// <copyright file="AsyncCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Commands +{ + using System; + using System.Management.Automation; + using System.Threading; + using System.Threading.Tasks; + + /// <summary> + /// This is the base class for any command that performs async operations. + /// It supports running tasks in an MTA thread via RunOnMta. + /// If the thread is already running on an MTA it will executed it, otherwise + /// it will create a new MTA thread. + /// + /// 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 readonly Thread originalThread; + + private readonly SemaphoreSlim semaphore = new (1, 1); + private readonly ManualResetEventSlim mainThreadActionReady = new (false); + private readonly ManualResetEventSlim mainThreadActionCompleted = new (false); + private readonly CancellationToken cancellationToken; + + private readonly bool isDebugBounded; + + private Action? mainThreadAction = null; + private bool canWriteToStream; + + /// <summary> + /// Initializes a new instance of the <see cref="AsyncCommand"/> class. + /// </summary> + /// <param name="psCmdlet">PSCmdlet.</param> + /// <param name="cancellationToken">Cancellation token.</param> + /// <param name="canWriteToStream">If the command can write to stream.</param> + public AsyncCommand(PSCmdlet psCmdlet, CancellationToken cancellationToken, bool canWriteToStream) + { + this.PsCmdlet = psCmdlet; + this.originalThread = Thread.CurrentThread; + this.cancellationToken = cancellationToken; + this.isDebugBounded = this.PsCmdlet.MyInvocation.BoundParameters.ContainsKey("Debug"); + this.canWriteToStream = canWriteToStream; + } + + /// <summary> + /// Gets the base cmdlet. + /// </summary> + 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. + /// </summary> + private bool CanWriteToStream + { + get + { + lock (CmdletLock) + { + return this.canWriteToStream; + } + } + + set + { + lock (CmdletLock) + { + this.canWriteToStream = value; + } + } + } + + /// <summary> + /// Execute the delegate in a MTA thread. + /// </summary> + /// <param name="func">Function to execute.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + public Task RunOnMTA(Func<Task> func) + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) + { + this.WriteDebug("Already running on MTA"); + return func(); + } + + this.WriteDebug("Creating MTA thread"); + var tcs = new TaskCompletionSource(); + var thread = new Thread(() => + { + try + { + func().GetAwaiter().GetResult(); + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + + thread.SetApartmentState(ApartmentState.MTA); + thread.Start(); + return tcs.Task; + } + + /// <summary> + /// Execute the delegate in a MTA thread. + /// </summary> + /// <param name="func">Function to execute.</param> + /// <typeparam name="TResult">Return type of function.</typeparam> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + public Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func) + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) + { + this.WriteDebug("Already running on MTA"); + return func(); + } + + this.WriteDebug("Creating MTA thread"); + var tcs = new TaskCompletionSource<TResult>(); + var thread = new Thread(() => + { + try + { + var result = func().GetAwaiter().GetResult(); + tcs.SetResult(result); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + + thread.SetApartmentState(ApartmentState.MTA); + thread.Start(); + return tcs.Task; + } + + /// <summary> + /// Waits for the task to be completed. This MUST be called from the main thread. + /// </summary> + /// <param name="runningTask">Task to wait for.</param> + public void Wait(Task runningTask) + { + // This must be called in the main thread. + if (this.originalThread != Thread.CurrentThread) + { + throw new InvalidOperationException(); + } + + this.canWriteToStream = true; + 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(); + } + } + while (!runningTask.IsCompleted); + + if (runningTask.IsFaulted) + { + // If IsFaulted is true, the task's Status is equal to Faulted, + // and its Exception property will be non-null. + throw runningTask.Exception!; + } + } + + /// <summary> + /// 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> + public void WriteDebug(string text) + { + if (!this.CanWriteToStream) + { + return; + } + + // Don't do context switch if no need. + if (!this.isDebugBounded) + { + 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 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">Warning text.</param> + public void WriteWarning(string text) + { + if (!this.CanWriteToStream) + { + 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; + } + } + + /// <summary> + /// Calls cmdlet WriteObject. + /// </summary> + /// <param name="obj">Object to write.</param> + public void WriteObject(object obj) + { + if (this.originalThread == Thread.CurrentThread) + { + this.PsCmdlet.WriteObject(obj); + return; + } + + try + { + this.WaitForOurTurn(); + this.mainThreadAction = () => this.PsCmdlet.WriteObject(obj); + this.mainThreadActionReady.Set(); + this.WaitMainThreadActionCompletion(); + } + catch (Exception) + { + throw; + } + } + + private void WaitForOurTurn() + { + this.semaphore.Wait(this.cancellationToken); + this.mainThreadActionCompleted.Reset(); + } + + private void WaitMainThreadActionCompletion() + { + WaitHandle.WaitAny(new[] + { + this.cancellationToken.WaitHandle, + this.mainThreadActionCompleted.WaitHandle, + }); + + this.semaphore.Release(); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs @@ -13,74 +13,266 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands using System.Threading.Tasks; using Microsoft.Management.Configuration; using Microsoft.Management.Configuration.Processor; + using Microsoft.PowerShell; + using Microsoft.WinGet.Configuration.Engine.PSObjects; using Windows.Storage; using Windows.Storage.Streams; /// <summary> - /// Class that deals with start and invoke the configuration. + /// Class that deals configuration commands. /// </summary> - public sealed class ConfigurationCommand : MtaCommand + public sealed class ConfigurationCommand : AsyncCommand { - private readonly string configFile; - /// <summary> /// Initializes a new instance of the <see cref="ConfigurationCommand"/> class. /// </summary> /// <param name="psCmdlet">PSCmdlet.</param> /// <param name="cancellationToken">Cancellation token.</param> + /// <param name="canWriteToStream">If the command can write to stream.</param> + public ConfigurationCommand(PSCmdlet psCmdlet, CancellationToken cancellationToken, bool canWriteToStream = true) + : base(psCmdlet, cancellationToken, canWriteToStream) + { + } + + /// <summary> + /// Open a configuration set. + /// </summary> /// <param name="configFile">Configuration file path.</param> - public ConfigurationCommand(PSCmdlet psCmdlet, CancellationToken cancellationToken, string configFile) - : base(psCmdlet, cancellationToken) + /// <param name="executionPolicy">Execution policy.</param> + public void Get(string configFile, ExecutionPolicy executionPolicy) { + if (!Path.IsPathRooted(configFile)) + { + configFile = Path.GetFullPath( + Path.Combine( + this.PsCmdlet.SessionState.Path.CurrentFileSystemLocation.Path, + configFile)); + } + if (!File.Exists(configFile)) { throw new FileNotFoundException(configFile); } - this.configFile = configFile; + // Start task. + var runningTask = this.RunOnMTA<PSConfigurationSet>( + async () => await this.OpenConfigurationSetAsync(configFile, executionPolicy)); + + this.Wait(runningTask); + this.WriteObject(runningTask.Result); } /// <summary> - /// Invoke configuration. Waits until completed. + /// Gets the details of a configuration set. /// </summary> - public void Invoke() + /// <param name="psConfigurationSet">PSConfigurationSet.</param> + public void GetDetails(PSConfigurationSet psConfigurationSet) { - // Start task. - var runningTask = this.RunOnMTA(this.InvokeAsync); + if (!psConfigurationSet.HasDetails) + { + if (!psConfigurationSet.CanProcess()) + { + // TODO: better exception or just write info and return null. + throw new Exception("Someone is using me!!!"); + } - // Wait for it to complete or being cancelled. - this.Wait(runningTask); + var runningTask = this.RunOnMTA<PSConfigurationSet>( + async () => + { + try + { + psConfigurationSet = await this.GetSetDetailsAsync(psConfigurationSet); + } + finally + { + psConfigurationSet.DoneProcessing(); + } + + return psConfigurationSet; + }); + + this.Wait(runningTask); + psConfigurationSet = runningTask.Result; + } + else + { + this.WriteWarning("Details already obtained for this set"); + } + + this.WriteObject(psConfigurationSet); } - private async Task InvokeAsync() + /// <summary> + /// Starts configuration. + /// </summary> + /// <param name="psConfigurationSet">PSConfigurationSet.</param> + public void StartApply(PSConfigurationSet psConfigurationSet) { - if (Thread.CurrentThread.GetApartmentState() != ApartmentState.MTA) + if (psConfigurationSet.Set.State == ConfigurationSetState.Completed) + { + this.WriteWarning("Processing this set is completed"); + return; + } + + if (!psConfigurationSet.CanProcess()) { - throw new NotSupportedException("Calling from an STA"); + // TODO: better exception or just write info and return null. + throw new Exception("Someone is using me!!!"); + } + + var configurationJob = this.StartApplyInternal(psConfigurationSet); + this.WriteObject(configurationJob); + } + + /// <summary> + /// Applies configuration. + /// </summary> + /// <param name="psConfigurationSet">PSConfigurationSet.</param> + public void Apply(PSConfigurationSet psConfigurationSet) => this.ContinueHelper(this.StartApplyInternal(psConfigurationSet)); + + /// <summary> + /// Continue a configuration job. + /// </summary> + /// <param name="psConfigurationJob">The configuration job.</param> + public void Continue(PSConfigurationJob psConfigurationJob) + { + if (psConfigurationJob.ConfigurationTask.IsCompleted) + { + this.WriteDebug("The task was completed before waiting"); + if (psConfigurationJob.ConfigurationTask.IsCompletedSuccessfully) + { + this.WriteDebug("Completed successfully"); + this.WriteObject(psConfigurationJob.ConfigurationTask.Result); + return; + } + else if (psConfigurationJob.ConfigurationTask.IsFaulted) + { + this.WriteDebug("Completed faulted before waiting"); + + // Maybe just write error? + throw psConfigurationJob.ConfigurationTask.Exception!; + } } - // This will fail with E_NOTIMPL + this.ContinueHelper(psConfigurationJob); + } + + 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"); + psConfigurationJob.StartCommand.Wait(psConfigurationJob.ConfigurationTask); + this.WriteObject(psConfigurationJob.ConfigurationTask.Result); + } + + private ConfigurationProcessor CreateConfigurationProcessor(ExecutionPolicy executionPolicy) + { + var properties = new ConfigurationProcessorFactoryProperties(); + properties.Policy = this.GetConfigurationProcessorPolicy(executionPolicy); + var factory = new ConfigurationSetProcessorFactory( - ConfigurationProcessorType.Default, null); + ConfigurationProcessorType.Default, properties); + var processor = new ConfigurationProcessor(factory); - var configSet = await this.CreateConfigurationSetAsync(processor); - _ = await processor.GetSetDetailsAsync(configSet, ConfigurationUnitDetailLevel.Catalog); + // TODO: set up logging and telemetry. + ////processor.MinimumLevel = DiagnosticLevel.Error; + ////processor.Caller = "ConfigurationModule"; + ////processor.ActivityIdentifier = Guid.NewGuid(); + ////processor.GenerateTelemetryEvents = false; + ////processor.Diagnostics; + + return processor; } - private async Task<ConfigurationSet> CreateConfigurationSetAsync(ConfigurationProcessor processor) + private async Task<PSConfigurationSet> OpenConfigurationSetAsync(string configFile, ExecutionPolicy executionPolicy) { - var stream = await FileRandomAccessStream.OpenAsync(this.configFile, FileAccessMode.Read); + var processor = this.CreateConfigurationProcessor(executionPolicy); + var stream = await FileRandomAccessStream.OpenAsync(configFile, FileAccessMode.Read); OpenConfigurationSetResult openResult = await processor.OpenConfigurationSetAsync(stream); - if (openResult.Set is null) { // TODO: throw better exception. throw new Exception($"Failed opening configuration set. Result 0x{openResult.ResultCode} at {openResult.Field}"); } - return openResult.Set; + var set = openResult.Set; + + // This should match winget's OpenConfigurationSet or OpenConfigurationSetAsync + // should be modify to take the full path and handle it. + set.Name = Path.GetFileName(configFile); + set.Origin = Path.GetDirectoryName(configFile); + set.Path = configFile; + + return new PSConfigurationSet(processor, set); + } + + private PSConfigurationJob StartApplyInternal(PSConfigurationSet psConfigurationSet) + { + var runningTask = this.RunOnMTA<PSConfigurationSet>( + async () => + { + try + { + psConfigurationSet = await this.ApplyConfigurationAsync(psConfigurationSet); + } + finally + { + psConfigurationSet.DoneProcessing(); + } + + return psConfigurationSet; + }); + + return new PSConfigurationJob(runningTask, this); + } + + private async Task<PSConfigurationSet> ApplyConfigurationAsync(PSConfigurationSet psConfigurationSet) + { + if (!psConfigurationSet.HasDetails) + { + this.WriteDebug("Getting details for configuration set"); + await this.GetSetDetailsAsync(psConfigurationSet); + } + + var processor = psConfigurationSet.Processor; + var set = psConfigurationSet.Set; + + // TODO: implement progress + _ = await processor.ApplySetAsync(set, ApplyConfigurationSetFlags.None); + + return psConfigurationSet; + } + + private async Task<PSConfigurationSet> GetSetDetailsAsync(PSConfigurationSet psConfigurationSet) + { + var processor = psConfigurationSet.Processor; + var set = psConfigurationSet.Set; + + if (set.ConfigurationUnits.Count == 0) + { + this.WriteWarning("Configuration File Empty"); + } + + // TODO: implement progress + _ = await processor.GetSetDetailsAsync(set, ConfigurationUnitDetailLevel.Catalog); + + psConfigurationSet.HasDetails = true; + return psConfigurationSet; + } + + private ConfigurationProcessorPolicy GetConfigurationProcessorPolicy(ExecutionPolicy policy) + { + return policy switch + { + ExecutionPolicy.Unrestricted => ConfigurationProcessorPolicy.Unrestricted, + ExecutionPolicy.RemoteSigned => ConfigurationProcessorPolicy.RemoteSigned, + ExecutionPolicy.AllSigned => ConfigurationProcessorPolicy.AllSigned, + ExecutionPolicy.Restricted => ConfigurationProcessorPolicy.Restricted, + ExecutionPolicy.Bypass => ConfigurationProcessorPolicy.Bypass, + _ => throw new InvalidOperationException(), + }; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/MtaCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/MtaCommand.cs @@ -1,222 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="MtaCommand.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Configuration.Engine.Commands -{ - using System; - using System.Management.Automation; - using System.Threading; - using System.Threading.Tasks; - - /// <summary> - /// This is the base class for any command that needs to be executed on an MTA thread. - /// Call RunOnMTA to start any async function call. 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. - /// </summary> - public abstract class MtaCommand - { - 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 CancellationToken cancellationToken; - - private Action? mainThreadAction = null; - - /// <summary> - /// Initializes a new instance of the <see cref="MtaCommand"/> class. - /// </summary> - /// <param name="psCmdlet">PSCmdlet.</param> - /// <param name="cancellationToken">Cancellation token.</param> - public MtaCommand(PSCmdlet psCmdlet, CancellationToken cancellationToken) - { - this.PsCmdlet = psCmdlet; - this.originalThread = Thread.CurrentThread; - this.cancellationToken = cancellationToken; - } - - /// <summary> - /// Gets the base cmdlet. - /// </summary> - protected PSCmdlet PsCmdlet { get; private set; } - - /// <summary> - /// Execute the delegate in a MTA thread. - /// </summary> - /// <param name="func">Function to execute.</param> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - public Task RunOnMTA(Func<Task> func) - { - // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) - { - throw new InvalidOperationException(); - } - - if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) - { - this.WriteDebug("Already running on MTA"); - return func(); - } - - this.WriteDebug("Creating MTA thread"); - var tcs = new TaskCompletionSource(); - var thread = new Thread(() => - { - try - { - func().GetAwaiter().GetResult(); - tcs.SetResult(); - } - catch (Exception e) - { - tcs.SetException(e); - } - }); - - thread.SetApartmentState(ApartmentState.MTA); - thread.Start(); - return tcs.Task; - } - - /// <summary> - /// Execute the delegate in a MTA thread. - /// </summary> - /// <param name="func">Function to execute.</param> - /// <typeparam name="TResult">Return type of function.</typeparam> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - public Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func) - where TResult : struct - { - // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) - { - throw new InvalidOperationException(); - } - - if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) - { - this.WriteDebug("Already running on MTA"); - return func(); - } - - this.WriteDebug("Creating MTA thread"); - var tcs = new TaskCompletionSource<TResult>(); - var thread = new Thread(() => - { - try - { - var result = func().GetAwaiter().GetResult(); - tcs.SetResult(result); - } - catch (Exception e) - { - tcs.SetException(e); - } - }); - - thread.SetApartmentState(ApartmentState.MTA); - thread.Start(); - return tcs.Task; - } - - /// <summary> - /// Waits for the task to be completed. This MUST be called from the main thread. - /// </summary> - /// <param name="runningTask">Task to wait for.</param> - public void Wait(Task runningTask) - { - // This must be called in the main thread. - if (this.originalThread != Thread.CurrentThread) - { - throw new InvalidOperationException(); - } - - 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(); - } - } - while (!runningTask.IsCompleted); - - if (runningTask.IsFaulted) - { - // If IsFaulted is true, the task's Status is equal to Faulted, - // and its Exception property will be non-null. - throw runningTask.Exception!; - } - } - - /// <summary> - /// 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> - protected void WriteDebug(string text) - { - 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; - } - } - - private void WaitForOurTurn() - { - this.semaphore.Wait(this.cancellationToken); - this.mainThreadActionCompleted.Reset(); - } - - private void WaitMainThreadActionCompletion() - { - WaitHandle.WaitAny(new[] - { - this.cancellationToken.WaitHandle, - this.mainThreadActionCompleted.WaitHandle, - }); - - this.semaphore.Release(); - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs @@ -0,0 +1,50 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSConfigurationJob.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using System.Threading.Tasks; + using Microsoft.WinGet.Configuration.Engine.Commands; + + /// <summary> + /// This is a wrapper object for asynchronous task for this module. + /// Contains the necessary information to continue the operation. + /// </summary> + public class PSConfigurationJob + { + /// <summary> + /// Initializes a new instance of the <see cref="PSConfigurationJob"/> class. + /// </summary> + /// <param name="configTask">The configuration task.</param> + /// <param name="startCommand">The start command.</param> + internal PSConfigurationJob( + Task<PSConfigurationSet> configTask, + AsyncCommand startCommand) + { + this.ConfigurationTask = configTask; + this.StartCommand = startCommand; + } + + /// <summary> + /// Gets the running configuration task. + /// </summary> + internal Task<PSConfigurationSet> ConfigurationTask { get; private set; } + + /// <summary> + /// Gets the command that started async operation. + /// </summary> + internal AsyncCommand StartCommand { get; private set; } + + /// <summary> + /// Gets the status of the configuration task. + /// </summary> + /// <returns>The task status.</returns> + public string GetStatus() + { + return this.ConfigurationTask.Status.ToString(); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs @@ -0,0 +1,147 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSConfigurationSet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using Microsoft.Management.Configuration; + + /// <summary> + /// Wrapper for ConfigurationSet. + /// </summary> + public sealed class PSConfigurationSet + { + private static readonly object ProcessorLock = new (); + private volatile bool hasDetails = false; + private volatile bool operationInProgress = false; + + /// <summary> + /// Initializes a new instance of the <see cref="PSConfigurationSet"/> class. + /// </summary> + /// <param name="processor">The configuration processor.</param> + /// <param name="set">The configuration set.</param> + internal PSConfigurationSet(ConfigurationProcessor processor, ConfigurationSet set) + { + this.Processor = processor; + this.Set = set; + } + + /// <summary> + /// Gets the name. + /// </summary> + public string Name + { + get + { + return this.Set.Name; + } + } + + /// <summary> + /// Gets the origin. + /// </summary> + public string Origin + { + get + { + return this.Set.Origin; + } + } + + /// <summary> + /// Gets the source. + /// </summary> + public string Source + { + get + { + return this.Set.Path; + } + } + + /// <summary> + /// Gets the state. + /// </summary> + public string State + { + get + { + return this.Set.State.ToString(); + } + } + + /// <summary> + /// Gets the schema version. + /// </summary> + public string SchemaVersion + { + get + { + return this.Set.SchemaVersion; + } + } + + /// <summary> + /// Gets the ConfigurationProcessor. + /// </summary> + internal ConfigurationProcessor Processor { get; private set; } + + /// <summary> + /// Gets the ConfigurationSet. + /// </summary> + internal ConfigurationSet Set { get; private set; } + + /// <summary> + /// Gets or sets a value indicating whether the details had been retrieved for this set. + /// </summary> + internal bool HasDetails + { + get + { + lock (ProcessorLock) + { + return this.hasDetails; + } + } + + set + { + lock (ProcessorLock) + { + this.hasDetails = value; + } + } + } + + /// <summary> + /// Checks if the object is being used by another cmdlet. If not, blocks it for the caller. + /// </summary> + /// <returns>True if no one is using me.</returns> + internal bool CanProcess() + { + lock (ProcessorLock) + { + if (!this.operationInProgress) + { + this.operationInProgress = true; + return true; + } + + return false; + } + } + + /// <summary> + /// The object is no longer in use by a cmdlet. + /// </summary> + internal void DoneProcessing() + { + lock (ProcessorLock) + { + this.operationInProgress = false; + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 b/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 @@ -15,7 +15,11 @@ FunctionsToExport = @() AliasesToExport = @() CmdletsToExport = @( + "Complete-WinGetConfiguration" + "Get-WinGetConfiguration" + "Get-WinGetConfigurationDetails" "Invoke-WinGetConfiguration" + "Start-WinGetConfiguration" ) PrivateData = @{