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 }