162 private static readonly
char[] s_pathSeparators = { EnvironmentInfo.IsWin ?
';' :
':' };
163 private static readonly
object s_lock =
new();
165 private readonly
string _toolPath;
169 _toolPath = toolPath;
172 public IReadOnlyCollection<Output>? Execute(
174 ArgumentStringHandler arguments =
default,
175 string? workingDirectory =
null,
176 IReadOnlyDictionary<string, string>? environmentVariables =
null,
178 bool? logOutput =
null,
179 bool? logInvocation =
null,
180 Action<OutputType, string>? logger =
null,
181 Action<IProcess>? exitHandler =
null,
184 Action<StreamWriter>? input =
null,
185 Encoding? standardOutputEncoding =
null,
186 Encoding? standardInputEncoding =
null
189 workingDirectory ??= EnvironmentInfo.WorkingDirectory;
190 logInvocation ??=
true;
192 logger ??= ProcessTasks.DefaultLogger;
193 standardOutputEncoding ??= Encoding.UTF8;
194 standardInputEncoding ??=
new UTF8Encoding(
false);
195 var outputFilter = arguments.GetFilter();
197 var toolPath = _toolPath;
198 var args = arguments.ToStringAndClear();
200 if (!Path.IsPathRooted(_toolPath) && !_toolPath.Contains(Path.DirectorySeparatorChar))
201 toolPath = ToolPathResolver.GetPathExecutable(_toolPath);
203 var toolPathOverride = GetToolPathOverride(toolPath);
204 if (!
string.IsNullOrEmpty(toolPathOverride))
206 args = $
"{toolPath.DoubleQuoteIfNeeded()} {args}".TrimEnd();
207 toolPath = toolPathOverride;
210 Assert.FileExists(toolPath);
211 Assert.DirectoryExists(workingDirectory);
213 var startInfo =
new ProcessStartInfo
217 WorkingDirectory = workingDirectory,
218 RedirectStandardOutput =
true,
219 RedirectStandardError =
true,
220 RedirectStandardInput = input !=
null,
221 UseShellExecute =
false,
222 StandardErrorEncoding = standardOutputEncoding,
223 StandardOutputEncoding = standardOutputEncoding,
226 startInfo.StandardInputEncoding = standardInputEncoding;
228 if (environmentVariables !=
null)
230 startInfo.Environment.Clear();
231 foreach (var (key, value) in environmentVariables)
232 startInfo.Environment[key] = value;
235 if (logInvocation.Value)
236 LogInvocation(startInfo, outputFilter);
238 var process = Process.Start(startInfo);
242 input?.Invoke(process.StandardInput);
244 var output = GetOutputCollection(process, logger, outputFilter);
245 var proc2 =
new Process2(process, outputFilter, timeout, output);
247 (exitHandler ?? (p => p.AssertZeroExitCode())).Invoke(proc2.AssertWaitForExit());
251 private static string? GetToolPathOverride(
string toolPath)
253 if (toolPath.EndsWithOrdinalIgnoreCase(
".dll"))
255 return ToolPathResolver.TryGetEnvironmentExecutable(
"DOTNET_EXE") ??
256 ToolPathResolver.GetPathExecutable(
"dotnet");
259 if (EnvironmentInfo.IsUnix &&
260 toolPath.EndsWithOrdinalIgnoreCase(
".exe") &&
261 !EnvironmentInfo.IsWsl)
262 return ToolPathResolver.GetPathExecutable(
"mono");
267 private static BlockingCollection<Output> GetOutputCollection(
269 Action<OutputType, string>? logger,
270 Func<string, string> outputFilter)
272 var output =
new BlockingCollection<Output>();
274 process.OutputDataReceived += (_, e) =>
279 var filteredOutput = outputFilter(e.Data);
280 output.Add(
new Output { Text = filteredOutput, Type = OutputType.Std });
281 logger?.Invoke(OutputType.Std, filteredOutput);
283 process.ErrorDataReceived += (_, e) =>
288 var filteredOutput = outputFilter(e.Data);
289 output.Add(
new Output { Text = filteredOutput, Type = OutputType.Err });
290 logger?.Invoke(OutputType.Err, filteredOutput);
293 process.BeginOutputReadLine();
294 process.BeginErrorReadLine();
299 private static void LogInvocation(ProcessStartInfo startInfo, Func<string, string> outputFilter)
303 Log.Information(
"> {ToolPath} {Arguments}", startInfo.FileName.DoubleQuoteIfNeeded(), outputFilter(startInfo.Arguments));
305 startInfo.WorkingDirectory != EnvironmentInfo.WorkingDirectory
306 ? LogEventLevel.Information
307 : LogEventLevel.Verbose,
308 "@ {WorkingDirectory}",
309 startInfo.WorkingDirectory);