winget-cli

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

ProcessExecution.cs (10649B)


      1 // -----------------------------------------------------------------------------
      2 // <copyright file="ProcessExecution.cs" company="Microsoft Corporation">
      3 //     Copyright (c) Microsoft Corporation. Licensed under the MIT License.
      4 // </copyright>
      5 // -----------------------------------------------------------------------------
      6 
      7 namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers
      8 {
      9     using System;
     10     using System.Collections.Generic;
     11     using System.Diagnostics;
     12     using System.Text;
     13     using System.Threading;
     14     using Microsoft.Management.Configuration.Processor.Helpers;
     15 
     16     /// <summary>
     17     /// Wrapper for a single process execution and its output.
     18     /// </summary>
     19     internal class ProcessExecution
     20     {
     21         private List<string> outputLines = new List<string>();
     22         private List<string> errorLines = new List<string>();
     23 
     24         /// <summary>
     25         /// Initializes a new instance of the <see cref="ProcessExecution"/> class.
     26         /// </summary>
     27         public ProcessExecution()
     28         {
     29         }
     30 
     31         /// <summary>
     32         /// An event that receives the output lines as they are delivered.
     33         /// </summary>
     34         public event EventHandler<string>? OutputLineReceived;
     35 
     36         /// <summary>
     37         /// An event that receives the error lines as they are delivered.
     38         /// </summary>
     39         public event EventHandler<string>? ErrorLineReceived;
     40 
     41         /// <summary>
     42         /// Gets the executable path.
     43         /// </summary>
     44         required public string ExecutablePath { get; init; }
     45 
     46         /// <summary>
     47         /// Gets the arguments to use for the process.
     48         /// </summary>
     49         [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")]
     50         public IEnumerable<string> Arguments { get; init; } = [];
     51 
     52         /// <summary>
     53         /// Gets the data to write to standard input of the process.
     54         /// </summary>
     55         public string? Input { get; init; } = null;
     56 
     57         /// <summary>
     58         /// Gets the list of custom environment variables to use for the process.
     59         /// </summary>
     60         [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")]
     61         public IEnumerable<ProcessExecutionEnvironmentVariable> EnvironmentVariables { get; init; } = [];
     62 
     63         /// <summary>
     64         /// Gets the argument string passed to the process.
     65         /// </summary>
     66         public string SerializedArguments
     67         {
     68             get
     69             {
     70                 StringBuilder processArguments = new StringBuilder();
     71 
     72                 foreach (string arg in this.Arguments)
     73                 {
     74                     if (processArguments.Length != 0)
     75                     {
     76                         processArguments.Append(' ');
     77                     }
     78 
     79                     processArguments.Append(arg);
     80                 }
     81 
     82                 return processArguments.ToString();
     83             }
     84         }
     85 
     86         /// <summary>
     87         /// Gets the full command line that the process should see.
     88         /// </summary>
     89         public string CommandLine
     90         {
     91             get
     92             {
     93                 return $"{this.ExecutablePath} {this.SerializedArguments}";
     94             }
     95         }
     96 
     97         /// <summary>
     98         /// Gets the current set of output lines.
     99         /// Not thread safe, use OutputLineReceived for async flows.
    100         /// </summary>
    101         public IReadOnlyCollection<string> Output
    102         {
    103             get { return this.outputLines; }
    104         }
    105 
    106         /// <summary>
    107         /// Gets the current set of error lines.
    108         /// Not thread safe, use ErrorLineReceived for async flows.
    109         /// </summary>
    110         public IReadOnlyCollection<string> Error
    111         {
    112             get { return this.errorLines; }
    113         }
    114 
    115         /// <summary>
    116         /// Gets the exit code of the process.
    117         /// Will be null until the process exits.
    118         /// </summary>
    119         public int? ExitCode { get; private set; } = null;
    120 
    121         /// <summary>
    122         /// Gets or sets the process object; null until Start called.
    123         /// </summary>
    124         private Process? Process { get; set; }
    125 
    126         /// <summary>
    127         /// Starts the process.
    128         /// </summary>
    129         /// <returns>This object.</returns>
    130         /// <exception cref="InvalidOperationException">Thrown if Start has already been called.</exception>
    131         public ProcessExecution Start()
    132         {
    133             if (this.Process != null)
    134             {
    135                 throw new InvalidOperationException("Process has already been started.");
    136             }
    137 
    138             ProcessStartInfo startInfo;
    139 
    140             lock (PathEnvironmentVariableHandler.Lock)
    141             {
    142                 startInfo = new ProcessStartInfo(this.ExecutablePath, this.SerializedArguments);
    143             }
    144 
    145             this.Process = new Process() { StartInfo = startInfo };
    146 
    147             startInfo.UseShellExecute = false;
    148             startInfo.WindowStyle = ProcessWindowStyle.Hidden;
    149 
    150             startInfo.StandardOutputEncoding = Encoding.UTF8;
    151             startInfo.RedirectStandardOutput = true;
    152             this.Process.OutputDataReceived += (sender, args) =>
    153             {
    154                 string? output = args.Data;
    155 
    156                 if (output != null)
    157                 {
    158                     this.outputLines.Add(output);
    159 
    160                     this.OutputLineReceived?.Invoke(this, output);
    161                 }
    162             };
    163 
    164             startInfo.StandardErrorEncoding = Encoding.UTF8;
    165             startInfo.RedirectStandardError = true;
    166             this.Process.ErrorDataReceived += (sender, args) =>
    167             {
    168                 string? error = args.Data;
    169 
    170                 if (error != null)
    171                 {
    172                     this.errorLines.Add(error);
    173 
    174                     this.ErrorLineReceived?.Invoke(this, error);
    175                 }
    176             };
    177 
    178             if (this.Input != null)
    179             {
    180                 startInfo.StandardInputEncoding = Encoding.UTF8;
    181                 startInfo.RedirectStandardInput = true;
    182             }
    183 
    184             foreach (var env in this.EnvironmentVariables)
    185             {
    186                 switch (env.ValueType)
    187                 {
    188                     case ProcessExecutionEnvironmentVariableValueType.Override:
    189                         startInfo.EnvironmentVariables[env.Name] = env.Value;
    190                         break;
    191 
    192                     case ProcessExecutionEnvironmentVariableValueType.Prepend:
    193                         startInfo.EnvironmentVariables[env.Name] = MergeStringsWithSeparator(env.Value, startInfo.EnvironmentVariables[env.Name] ?? string.Empty, env.Separator);
    194                         break;
    195 
    196                     case ProcessExecutionEnvironmentVariableValueType.Append:
    197                         startInfo.EnvironmentVariables[env.Name] = MergeStringsWithSeparator(startInfo.EnvironmentVariables[env.Name] ?? string.Empty, env.Value, env.Separator);
    198                         break;
    199                 }
    200             }
    201 
    202             this.Process.Start();
    203             this.Process.BeginOutputReadLine();
    204             this.Process.BeginErrorReadLine();
    205 
    206             if (this.Input != null)
    207             {
    208                 this.Process.StandardInput.Write(this.Input);
    209                 this.Process.StandardInput.Close();
    210             }
    211 
    212             return this;
    213         }
    214 
    215         /// <summary>
    216         /// Waits for the process to exit.
    217         /// </summary>
    218         /// <param name="milliseconds">The minimum amount of time to wait for the process to exit, in milliseconds.</param>
    219         /// <returns>True if the process exited; false if not.</returns>
    220         /// <exception cref="InvalidOperationException">Thrown if Start has not been called.</exception>
    221         public bool WaitForExit(int milliseconds = Timeout.Infinite)
    222         {
    223             if (this.Process == null)
    224             {
    225                 throw new InvalidOperationException("Process has not been started.");
    226             }
    227 
    228             if (this.Process.WaitForExit(milliseconds))
    229             {
    230                 // According to documentation, this extra call will ensure that the redirected streams have finished reading all of the data.
    231                 this.Process.WaitForExit();
    232 
    233                 this.ExitCode = this.Process.ExitCode;
    234 
    235                 return true;
    236             }
    237             else
    238             {
    239                 return false;
    240             }
    241         }
    242 
    243         /// <summary>
    244         /// Gets all of the output lines as a single string.
    245         /// </summary>
    246         /// <returns>The output lines as a string.</returns>
    247         public string GetAllOutputLines()
    248         {
    249             return GetAllLines(this.outputLines);
    250         }
    251 
    252         /// <summary>
    253         /// Gets all of the error lines as a single string.
    254         /// </summary>
    255         /// <returns>The error lines as a string.</returns>
    256         public string GetAllErrorLines()
    257         {
    258             return GetAllLines(this.errorLines);
    259         }
    260 
    261         private static string GetAllLines(List<string> lines)
    262         {
    263             StringBuilder stringBuilder = new StringBuilder();
    264 
    265             foreach (string line in lines)
    266             {
    267                 stringBuilder.AppendLine(line);
    268             }
    269 
    270             return stringBuilder.ToString();
    271         }
    272 
    273         private static string MergeStringsWithSeparator(string first, string second, string separator)
    274         {
    275             if (string.IsNullOrEmpty(separator))
    276             {
    277                 return first + second;
    278             }
    279             else
    280             {
    281                 if (first.EndsWith(separator) && second.StartsWith(separator))
    282                 {
    283                     return first + second.Substring(separator.Length);
    284                 }
    285                 else if (first.EndsWith(separator) || second.StartsWith(separator))
    286                 {
    287                     return first + second;
    288                 }
    289                 else
    290                 {
    291                     return first + separator + second;
    292                 }
    293             }
    294         }
    295     }
    296 }