winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

PowerShellCmdlet.cs (23583B)


      1 // -----------------------------------------------------------------------------
      2 // <copyright file="PowerShellCmdlet.cs" company="Microsoft Corporation">
      3 //     Copyright (c) Microsoft Corporation. Licensed under the MIT License.
      4 // </copyright>
      5 // -----------------------------------------------------------------------------
      6 
      7 namespace Microsoft.WinGet.Common.Command
      8 {
      9     using System;
     10     using System.Collections.Concurrent;
     11     using System.Collections.Generic;
     12     using System.Management.Automation;
     13     using System.Runtime.ExceptionServices;
     14     using System.Threading;
     15     using System.Threading.Tasks;
     16     using Microsoft.WinGet.Resources;
     17     using Microsoft.WinGet.SharedLib.Exceptions;
     18     using Microsoft.WinGet.SharedLib.PolicySettings;
     19 
     20     /// <summary>
     21     /// This must be the base class for every cmdlet for winget PowerShell modules.
     22     /// It supports:
     23     ///  - Async operations.
     24     ///  - Execute on an MTA. If the thread is already running on an MTA it will executed it, otherwise
     25     ///    it will create a new MTA thread.
     26     /// Wait must be used to synchronously wait con the task.
     27     /// </summary>
     28     public abstract class PowerShellCmdlet
     29     {
     30         private const string Debug = "Debug";
     31         private static readonly string[] WriteInformationTags = new string[] { "PSHOST" };
     32 
     33         private readonly PSCmdlet psCmdlet;
     34         private readonly Thread pwshThread;
     35 
     36         private readonly CancellationTokenSource source = new ();
     37         private readonly SemaphoreSlim semaphore = new (1, 1);
     38         private readonly ManualResetEventSlim pwshThreadActionReady = new (false);
     39         private readonly ManualResetEventSlim pwshThreadActionCompleted = new (false);
     40 
     41         private BlockingCollection<QueuedStream> queuedStreams = new ();
     42         private int progressActivityId = 0;
     43         private ConcurrentDictionary<int, ProgressRecordType> progressRecords = new ();
     44         private Action? pwshThreadAction = null;
     45         private ExceptionDispatchInfo? pwshThreadEdi = null;
     46 
     47         /// <summary>
     48         /// Initializes a new instance of the <see cref="PowerShellCmdlet"/> class.
     49         /// </summary>
     50         /// <param name="psCmdlet">PSCmdlet.</param>
     51         /// <param name="policies">Policies.</param>
     52         public PowerShellCmdlet(PSCmdlet psCmdlet, HashSet<Policy> policies)
     53         {
     54             // Passing Debug will make all the message actions to be Inquire. For async operations
     55             // and the current queue message implementation this doesn't make sense.
     56             // PowerShell will inquire for any message giving the impression that the task is
     57             // paused, but the async operation is still running.
     58             if (psCmdlet.MyInvocation.BoundParameters.ContainsKey(Debug))
     59             {
     60                 throw new NotSupportedException(Resources.DebugNotSupported);
     61             }
     62 
     63             this.ValidatePolicies(policies);
     64 
     65             this.psCmdlet = psCmdlet;
     66             this.pwshThread = Thread.CurrentThread;
     67         }
     68 
     69         /// <summary>
     70         /// Request cancellation for this command.
     71         /// </summary>
     72         public void Cancel()
     73         {
     74             this.source.Cancel();
     75         }
     76 
     77         /// <summary>
     78         /// Execute the delegate in a MTA thread.
     79         /// Caller must wait on task.
     80         /// </summary>
     81         /// <param name="func">Function to execute.</param>
     82         /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
     83         internal Task RunOnMTA(Func<Task> func)
     84         {
     85             // .NET 4.8 doesn't support TaskCompletionSource.
     86 #if POWERSHELL_WINDOWS
     87             throw new NotImplementedException();
     88 #else
     89             // This must be called in the main thread.
     90             if (this.pwshThread != Thread.CurrentThread)
     91             {
     92                 throw new InvalidOperationException();
     93             }
     94 
     95             if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA)
     96             {
     97                 this.Write(StreamType.Verbose, "Already running on MTA");
     98                 try
     99                 {
    100                     Task result = func();
    101                     result.ContinueWith((task) => this.Complete(), TaskContinuationOptions.ExecuteSynchronously);
    102                     return result;
    103                 }
    104                 catch
    105                 {
    106                     this.Complete();
    107                     throw;
    108                 }
    109             }
    110 
    111             this.Write(StreamType.Verbose, "Creating MTA thread");
    112             var tcs = new TaskCompletionSource();
    113             var thread = new Thread(() =>
    114             {
    115                 try
    116                 {
    117                     func().GetAwaiter().GetResult();
    118                     tcs.SetResult();
    119                 }
    120                 catch (Exception e)
    121                 {
    122                     tcs.SetException(e);
    123                 }
    124                 finally
    125                 {
    126                     this.Complete();
    127                 }
    128             });
    129 
    130             thread.SetApartmentState(ApartmentState.MTA);
    131             thread.Start();
    132             return tcs.Task;
    133 #endif
    134         }
    135 
    136         /// <summary>
    137         /// Execute the delegate in a MTA thread.
    138         /// Caller must wait on task.
    139         /// </summary>
    140         /// <param name="func">Function to execute.</param>
    141         /// <typeparam name="TResult">Return type of function.</typeparam>
    142         /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    143         internal Task<TResult> RunOnMTA<TResult>(Func<Task<TResult>> func)
    144         {
    145             // This must be called in the main thread.
    146             if (this.pwshThread != Thread.CurrentThread)
    147             {
    148                 throw new InvalidOperationException();
    149             }
    150 
    151             if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA)
    152             {
    153                 this.Write(StreamType.Verbose, "Already running on MTA");
    154                 try
    155                 {
    156                     Task<TResult> result = func();
    157                     result.ContinueWith((task) => this.Complete(), TaskContinuationOptions.ExecuteSynchronously);
    158                     return result;
    159                 }
    160                 catch
    161                 {
    162                     this.Complete();
    163                     throw;
    164                 }
    165             }
    166 
    167             this.Write(StreamType.Verbose, "Creating MTA thread");
    168             var tcs = new TaskCompletionSource<TResult>();
    169             var thread = new Thread(() =>
    170             {
    171                 try
    172                 {
    173                     var result = func().GetAwaiter().GetResult();
    174                     tcs.SetResult(result);
    175                 }
    176                 catch (Exception e)
    177                 {
    178                     tcs.SetException(e);
    179                 }
    180                 finally
    181                 {
    182                     this.Complete();
    183                 }
    184             });
    185 
    186             thread.SetApartmentState(ApartmentState.MTA);
    187             thread.Start();
    188             return tcs.Task;
    189         }
    190 
    191         /// <summary>
    192         /// Execute the delegate in a MTA thread.
    193         /// Synchronous call.
    194         /// </summary>
    195         /// <param name="func">Function to execute.</param>
    196         /// <typeparam name="TResult">Return type of function.</typeparam>
    197         /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    198         internal TResult RunOnMTA<TResult>(Func<TResult> func)
    199         {
    200             // This must be called in the main thread.
    201             if (this.pwshThread != Thread.CurrentThread)
    202             {
    203                 throw new InvalidOperationException();
    204             }
    205 
    206             if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA)
    207             {
    208                 this.Write(StreamType.Verbose, "Already running on MTA");
    209                 try
    210                 {
    211                     return func();
    212                 }
    213                 finally
    214                 {
    215                     this.Complete();
    216                 }
    217             }
    218 
    219             this.Write(StreamType.Verbose, "Creating MTA thread");
    220             var tcs = new TaskCompletionSource<TResult>();
    221             var thread = new Thread(() =>
    222             {
    223                 try
    224                 {
    225                     var result = func();
    226                     tcs.SetResult(result);
    227                 }
    228                 catch (Exception e)
    229                 {
    230                     tcs.SetException(e);
    231                 }
    232                 finally
    233                 {
    234                     this.Complete();
    235                 }
    236             });
    237 
    238             thread.SetApartmentState(ApartmentState.MTA);
    239             thread.Start();
    240             this.Wait(tcs.Task);
    241             return tcs.Task.Result;
    242         }
    243 
    244         /// <summary>
    245         /// Executes an action in the main thread.
    246         /// Blocks until call is executed.
    247         /// </summary>
    248         /// <param name="action">Action to perform.</param>
    249         internal void ExecuteInPowerShellThread(Action action)
    250         {
    251             if (this.pwshThread == Thread.CurrentThread)
    252             {
    253                 action();
    254                 return;
    255             }
    256 
    257             this.WaitForOurTurn();
    258 
    259             this.pwshThreadAction = action;
    260             this.pwshThreadActionReady.Set();
    261             this.WaitMainThreadActionCompletion();
    262         }
    263 
    264         /// <summary>
    265         /// Waits for the task to be completed. This MUST be called from the main thread.
    266         /// </summary>
    267         /// <param name="runningTask">Task to wait for.</param>
    268         /// <param name="writeCmdlet">The cmdlet that can write to PowerShell.</param>
    269         internal void Wait(Task runningTask, PowerShellCmdlet? writeCmdlet = null)
    270         {
    271             writeCmdlet ??= this;
    272 
    273             // This must be called in the main thread.
    274             if (this.pwshThread != Thread.CurrentThread)
    275             {
    276                 throw new InvalidOperationException();
    277             }
    278 
    279             do
    280             {
    281                 if (this.pwshThreadActionReady.IsSet)
    282                 {
    283                     // Someone needs the main thread.
    284                     this.pwshThreadActionReady.Reset();
    285 
    286                     if (this.pwshThreadAction != null)
    287                     {
    288                         try
    289                         {
    290                             this.pwshThreadEdi = null;
    291                             this.pwshThreadAction();
    292                         }
    293                         catch (Exception e)
    294                         {
    295                             // Make sure we don't throw in the PowerShell thread, this way
    296                             // we'll get a more meaningful stack by Get-Error.
    297                             this.pwshThreadEdi = ExceptionDispatchInfo.Capture(e);
    298                         }
    299 
    300                         this.pwshThreadAction = null;
    301                     }
    302 
    303                     // Done.
    304                     this.pwshThreadActionCompleted.Set();
    305                 }
    306 
    307                 // Take from the blocking collection.
    308                 if (!this.queuedStreams.IsCompleted && this.queuedStreams.Count > 0)
    309                 {
    310                     try
    311                     {
    312                         var queuedOutput = this.queuedStreams.Take();
    313                         if (queuedOutput != null)
    314                         {
    315                             this.CmdletWrite(queuedOutput.Type, queuedOutput.Data, writeCmdlet);
    316                         }
    317                     }
    318                     catch (InvalidOperationException)
    319                     {
    320                         // An InvalidOperationException means that Take() was called on a completed collection.
    321                     }
    322                 }
    323             }
    324             while (!(runningTask.IsCompleted && this.queuedStreams.IsCompleted));
    325 
    326             if (runningTask.IsFaulted)
    327             {
    328                 // If IsFaulted is true, the task's Status is equal to Faulted,
    329                 // and its Exception property will be non-null.
    330                 AggregateException? ae = runningTask.Exception! as AggregateException;
    331                 if (ae != null && ae.InnerExceptions.Count == 1)
    332                 {
    333                     ExceptionDispatchInfo.Capture(ae.InnerExceptions[0]).Throw();
    334                 }
    335 
    336                 throw runningTask.Exception!;
    337             }
    338         }
    339 
    340         /// <summary>
    341         /// Writes into the corresponding stream if running on the main thread.
    342         /// Otherwise queue the message.
    343         /// Is the caller responsibility to use the correct types.
    344         /// </summary>
    345         /// <param name="type">Stream type.</param>
    346         /// <param name="data">Data.</param>
    347         internal void Write(StreamType type, object data)
    348         {
    349             if (type == StreamType.Progress)
    350             {
    351                 ProgressRecord progressRecord = (ProgressRecord)data;
    352                 if (progressRecord.RecordType == ProgressRecordType.Completed)
    353                 {
    354                     throw new NotSupportedException("Use CompleteProgress");
    355                 }
    356 
    357                 // Keep track of all progress activity.
    358                 _ = this.progressRecords.TryAdd(progressRecord.ActivityId, progressRecord.RecordType);
    359             }
    360 
    361             if (this.pwshThread == Thread.CurrentThread)
    362             {
    363                 this.CmdletWrite(type, data, this);
    364                 return;
    365             }
    366 
    367             this.queuedStreams.Add(new QueuedStream(type, data));
    368         }
    369 
    370         /// <summary>
    371         /// Helper to compute percentage and write progress for processing activities.
    372         /// </summary>
    373         /// <param name="activityId">Activity id.</param>
    374         /// <param name="activity">The activity in progress.</param>
    375         /// <param name="status">The status of the activity.</param>
    376         /// <param name="completed">Number of completed actions.</param>
    377         /// <param name="total">The expected total.</param>
    378         internal void WriteProgressWithPercentage(int activityId, string activity, string status, int completed, int total)
    379         {
    380             double percentComplete = (double)completed / total;
    381             var record = new ProgressRecord(activityId, activity, status)
    382             {
    383                 RecordType = ProgressRecordType.Processing,
    384                 PercentComplete = (int)(100.0 * percentComplete),
    385             };
    386             this.Write(StreamType.Progress, record);
    387         }
    388 
    389         /// <summary>
    390         /// Helper to complete progress records.
    391         /// </summary>
    392         /// <param name="activityId">Activity id.</param>
    393         /// <param name="activity">The activity in progress.</param>
    394         /// <param name="status">The status of the activity.</param>
    395         /// <param name="force">Force write complete progress.</param>
    396         internal void CompleteProgress(int activityId, string activity, string status, bool force = false)
    397         {
    398             var record = new ProgressRecord(activityId, activity, status)
    399             {
    400                 RecordType = ProgressRecordType.Completed,
    401                 PercentComplete = 100,
    402             };
    403 
    404             if (!this.progressRecords.TryAdd(activityId, record.RecordType))
    405             {
    406                 _ = this.progressRecords.TryUpdate(activityId, record.RecordType, ProgressRecordType.Processing);
    407             }
    408 
    409             if (this.pwshThread == Thread.CurrentThread)
    410             {
    411                 this.CmdletWrite(StreamType.Progress, record, this);
    412             }
    413             else
    414             {
    415                 // You should only use force if you know the cmdlet that is completing this progress is a sync cmdlet that
    416                 // is running in an async context. A sync cmdlet is anything that doesn't start with Start-*
    417                 if (force)
    418                 {
    419                     this.ExecuteInPowerShellThread(() => this.CmdletWrite(StreamType.Progress, record, this));
    420                 }
    421                 else
    422                 {
    423                     this.queuedStreams.Add(new QueuedStream(StreamType.Progress, record));
    424                 }
    425             }
    426         }
    427 
    428         /// <summary>
    429         /// Writes to PowerShell streams.
    430         /// This method must be called in the original thread.
    431         /// WARNING: You must only call this when the task is completed.
    432         /// </summary>
    433         /// <param name="writeCmdlet">The cmdlet that can write to PowerShell.</param>
    434         internal void ConsumeAndWriteStreams(PowerShellCmdlet writeCmdlet)
    435         {
    436             // This must be called in the main thread.
    437             if (this.pwshThread != Thread.CurrentThread)
    438             {
    439                 throw new InvalidOperationException();
    440             }
    441 
    442             // Take from the blocking collection until is completed.
    443             try
    444             {
    445                 while (true)
    446                 {
    447                     var queuedOutput = this.queuedStreams.Take();
    448                     if (queuedOutput != null)
    449                     {
    450                         this.CmdletWrite(queuedOutput.Type, queuedOutput.Data, writeCmdlet);
    451                     }
    452                 }
    453             }
    454             catch (InvalidOperationException)
    455             {
    456                 // We are done.
    457                 // An InvalidOperationException means that Take() was called on a completed collection.
    458             }
    459         }
    460 
    461         /// <summary>
    462         /// Gets a new progress activity id.
    463         /// </summary>
    464         /// <returns>The new progress record id.</returns>
    465         internal int GetNewProgressActivityId()
    466         {
    467             return Interlocked.Increment(ref this.progressActivityId);
    468         }
    469 
    470         /// <summary>
    471         /// Gets the cancellation token.
    472         /// </summary>
    473         /// <returns>CancellationToken.</returns>
    474         internal CancellationToken GetCancellationToken()
    475         {
    476             return this.source.Token;
    477         }
    478 
    479         /// <summary>
    480         /// Gets the current file system location from the cmdlet.
    481         /// </summary>
    482         /// <returns>Path.</returns>
    483         internal string GetCurrentFileSystemLocation()
    484         {
    485             return this.psCmdlet.SessionState.Path.CurrentFileSystemLocation.Path;
    486         }
    487 
    488         /// <summary>
    489         /// Sets a variable.
    490         /// </summary>
    491         /// <param name="variableName">Variable name.</param>
    492         /// <param name="value">Value.</param>
    493         internal void SetVariable(string variableName, object value)
    494         {
    495             this.psCmdlet.SessionState.PSVariable.Set(variableName, value);
    496         }
    497 
    498         /// <summary>
    499         /// Prompts the user if it should continue processing if possible.
    500         /// </summary>
    501         /// <param name="target">Message.</param>
    502         /// <returns>If the operation should continue.</returns>
    503         internal bool ShouldProcess(string target)
    504         {
    505             // If not on the main thread just continue.
    506             if (this.pwshThread != Thread.CurrentThread)
    507             {
    508                 return true;
    509             }
    510 
    511             return this.psCmdlet.ShouldProcess(target);
    512         }
    513 
    514         private void Complete()
    515         {
    516             this.queuedStreams.CompleteAdding();
    517         }
    518 
    519         private void CmdletWrite(StreamType streamType, object data, PowerShellCmdlet writeCmdlet)
    520         {
    521             switch (streamType)
    522             {
    523                 case StreamType.Debug:
    524                     throw new NotSupportedException();
    525                 case StreamType.Verbose:
    526                     writeCmdlet.psCmdlet.WriteVerbose((string)data);
    527                     break;
    528                 case StreamType.Warning:
    529                     writeCmdlet.psCmdlet.WriteWarning((string)data);
    530                     break;
    531                 case StreamType.Error:
    532                     writeCmdlet.psCmdlet.WriteError((ErrorRecord)data);
    533                     break;
    534                 case StreamType.Progress:
    535                     // If the activity is already completed don't write progress.
    536                     var progressRecord = (ProgressRecord)data;
    537                     if (this.progressRecords[progressRecord.ActivityId] == progressRecord.RecordType)
    538                     {
    539                         writeCmdlet.psCmdlet.WriteProgress(progressRecord);
    540                     }
    541 
    542                     break;
    543                 case StreamType.Object:
    544                     writeCmdlet.psCmdlet.WriteObject(data);
    545                     break;
    546                 case StreamType.Information:
    547                     writeCmdlet.psCmdlet.WriteInformation(data, WriteInformationTags);
    548                     break;
    549             }
    550         }
    551 
    552         private void ValidatePolicies(HashSet<Policy> policies)
    553         {
    554             GroupPolicy groupPolicy = GroupPolicy.GetInstance();
    555 
    556             if (policies.Contains(Policy.WinGet))
    557             {
    558                 if (!groupPolicy.IsEnabled(Policy.WinGet))
    559                 {
    560                     throw new GroupPolicyException(Policy.WinGet, GroupPolicyFailureType.BlockedByPolicy);
    561                 }
    562 
    563                 policies.Remove(Policy.WinGet);
    564             }
    565 
    566             if (policies.Contains(Policy.Configuration))
    567             {
    568                 if (!groupPolicy.IsEnabled(Policy.Configuration))
    569                 {
    570                     throw new GroupPolicyException(Policy.Configuration, GroupPolicyFailureType.BlockedByPolicy);
    571                 }
    572 
    573                 policies.Remove(Policy.Configuration);
    574             }
    575 
    576             if (policies.Contains(Policy.WinGetCommandLineInterfaces))
    577             {
    578                 if (!groupPolicy.IsEnabled(Policy.WinGetCommandLineInterfaces))
    579                 {
    580                     throw new GroupPolicyException(Policy.WinGetCommandLineInterfaces, GroupPolicyFailureType.BlockedByPolicy);
    581                 }
    582 
    583                 policies.Remove(Policy.WinGetCommandLineInterfaces);
    584             }
    585 
    586             if (policies.Count > 0)
    587             {
    588                 throw new NotSupportedException($"Invalid policies {string.Join(",", policies)}");
    589             }
    590         }
    591 
    592         private void WaitForOurTurn()
    593         {
    594             this.semaphore.Wait(this.GetCancellationToken());
    595             this.pwshThreadActionCompleted.Reset();
    596         }
    597 
    598         private void WaitMainThreadActionCompletion()
    599         {
    600             WaitHandle.WaitAny(new[]
    601             {
    602                 this.GetCancellationToken().WaitHandle,
    603                 this.pwshThreadActionCompleted.WaitHandle,
    604             });
    605 
    606             try
    607             {
    608                 if (this.pwshThreadEdi != null)
    609                 {
    610                     this.pwshThreadEdi.Throw();
    611                 }
    612             }
    613             finally
    614             {
    615                 this.semaphore.Release();
    616             }
    617         }
    618 
    619         private class QueuedStream
    620         {
    621             public QueuedStream(StreamType type, object data)
    622             {
    623                 this.Type = type;
    624                 this.Data = data;
    625             }
    626 
    627             public StreamType Type { get; }
    628 
    629             public object Data { get; }
    630         }
    631     }
    632 }