172 private static readonly
object _lock =
new();
174 private readonly
string _toolPath;
176 private int _attempt = 0;
180 _toolPath = toolPath;
183 public IReadOnlyCollection<Output>? Execute(
186 string? workingDirectory =
null,
187 IReadOnlyDictionary<string, string>? environmentVariables =
null,
189 bool? logOutput =
null,
190 bool? logInvocation =
null,
191 Action<OutputType, string>? logger =
null,
192 Action<IProcess>? exitHandler =
null,
195 Action<StreamWriter>? input =
null,
196 Encoding? standardOutputEncoding =
null,
197 Encoding? standardInputEncoding =
null,
198 ToolExRetry? retry =
null
201 workingDirectory ??= EnvironmentInfo.WorkingDirectory;
202 logInvocation ??=
true;
204 logger ??= ProcessTasks.DefaultLogger;
205 standardOutputEncoding ??= Encoding.UTF8;
206 standardInputEncoding ??=
new UTF8Encoding(
false);
207 exitHandler ??= p => p.AssertZeroExitCode();
209 var outputFilter = arguments.GetFilter();
211 var toolPath = _toolPath;
212 var args = arguments.ToStringAndClear();
214 if (!Path.IsPathRooted(_toolPath) && !_toolPath.Contains(Path.DirectorySeparatorChar))
215 toolPath = ToolPathResolver.GetPathExecutable(_toolPath);
217 var toolPathOverride = GetToolPathOverride(toolPath);
218 if (!
string.IsNullOrEmpty(toolPathOverride))
220 args = $
"{toolPath.DoubleQuoteIfNeeded()} {args}".TrimEnd();
221 toolPath = toolPathOverride;
224 Assert.FileExists(toolPath);
225 Assert.DirectoryExists(workingDirectory);
227 var startInfo =
new ProcessStartInfo
231 WorkingDirectory = workingDirectory,
232 RedirectStandardOutput =
true,
233 RedirectStandardError =
true,
234 RedirectStandardInput = input !=
null,
235 UseShellExecute =
false,
236 StandardErrorEncoding = standardOutputEncoding,
237 StandardOutputEncoding = standardOutputEncoding,
240 startInfo.StandardInputEncoding = standardInputEncoding;
242 if (environmentVariables !=
null)
244 startInfo.Environment.Clear();
245 foreach (var (key, value) in environmentVariables)
246 startInfo.Environment[key] = value;
249 if (logInvocation.Value)
250 LogInvocation(startInfo, outputFilter);
252 var process = Process.Start(startInfo);
256 input?.Invoke(process.StandardInput);
258 var output = GetOutputCollection(process, logger, outputFilter);
259 var proc2 =
new Process2(process, outputFilter, timeout, output);
260 proc2.AssertWaitForExit();
264 var previousTool =
new ToolEx(Execute).With(
267 environmentVariables,
274 standardOutputEncoding,
275 standardInputEncoding,
278 var nextAttempt = retry(previousTool, proc2, ++_attempt);
279 if (nextAttempt !=
null)
281 return nextAttempt();
284 exitHandler.Invoke(proc2);
288 private static string? GetToolPathOverride(
string toolPath)
290 if (toolPath.EndsWithOrdinalIgnoreCase(
".dll"))
292 return ToolPathResolver.TryGetEnvironmentExecutable(
"DOTNET_EXE") ??
293 ToolPathResolver.GetPathExecutable(
"dotnet");
296 if (EnvironmentInfo.IsUnix &&
297 toolPath.EndsWithOrdinalIgnoreCase(
".exe") &&
298 !EnvironmentInfo.IsWsl)
299 return ToolPathResolver.GetPathExecutable(
"mono");
304 private static BlockingCollection<Output> GetOutputCollection(
306 Action<OutputType, string>? logger,
307 Func<string, string> outputFilter)
309 var output =
new BlockingCollection<Output>();
311 process.OutputDataReceived += (_, e) =>
316 var filteredOutput = outputFilter(e.Data);
317 output.Add(
new Output { Text = filteredOutput, Type = OutputType.Std });
318 logger?.Invoke(OutputType.Std, filteredOutput);
320 process.ErrorDataReceived += (_, e) =>
325 var filteredOutput = outputFilter(e.Data);
326 output.Add(
new Output { Text = filteredOutput, Type = OutputType.Err });
327 logger?.Invoke(OutputType.Err, filteredOutput);
330 process.BeginOutputReadLine();
331 process.BeginErrorReadLine();
336 private static void LogInvocation(ProcessStartInfo startInfo, Func<string, string> outputFilter)
340 Log.Information(
"> {ToolPath} {Arguments}", startInfo.FileName.DoubleQuoteIfNeeded(), outputFilter(startInfo.Arguments));
342 startInfo.WorkingDirectory != EnvironmentInfo.WorkingDirectory
343 ? LogEventLevel.Information
344 : LogEventLevel.Verbose,
345 "@ {WorkingDirectory}",
346 startInfo.WorkingDirectory);