using System.Diagnostics; namespace ReleaseBuilder; /// /// Helper methods for executing a commandline program /// public static class ProcessHelper { /// /// Starts a commandline program and waits for it to complete /// /// /// The working directory to run in; null means current directory /// The cancellation token /// Callback method that is invoked with the error code from the process; the result indicates if the status code should be interpreted as an error. /// Default value is null which will treat anything non-zero as an error /// If true, stderr is not forwarded to the console /// Function to write to stdin /// An awaitable task public static async Task Execute(IEnumerable command, string? workingDirectory = null, CancellationToken cancellationToken = default, Func? codeIsError = null, bool suppressStdErr = false, Func? writeStdIn = null) { if (!command.Any()) throw new ArgumentException("Needs at least one command", nameof(command)); workingDirectory ??= Environment.CurrentDirectory; if (!Directory.Exists(workingDirectory)) Directory.CreateDirectory(workingDirectory); codeIsError ??= (x) => x != 0; var p = Process.Start(new ProcessStartInfo(command.First(), command.Skip(1)) { WindowStyle = ProcessWindowStyle.Hidden, WorkingDirectory = workingDirectory, RedirectStandardError = !suppressStdErr, RedirectStandardOutput = false, RedirectStandardInput = writeStdIn != null, UseShellExecute = false, }) ?? throw new Exception($"Failed to launch process {command.First()}, null returned"); // Forward error messages to stderr var t = suppressStdErr ? Task.CompletedTask : p.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError(), cancellationToken); if (writeStdIn != null) await writeStdIn(p.StandardInput).ConfigureAwait(false); await p.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (codeIsError(p.ExitCode)) throw new Exception($"Execution of {command.First()} gave error code {p.ExitCode}"); await t.ConfigureAwait(false); } /// /// Runs all commandline tasks in sequence /// /// The commands to run /// The working directory to run in; null means current directory /// The cancellation token /// Callback method that is invoked with the error code from the process; the result indicates if the status code should be interpreted as an error. /// Default value is null which will treat anything non-zero as an error /// If true, stderr is not forwarded to the console /// An awaitable task public static async Task ExecuteAll(IEnumerable> commands, string? workingDirectory = null, CancellationToken cancellationToken = default, Func? codeIsError = null, bool suppressStdErr = false) { foreach (var c in commands) await Execute(c, workingDirectory, cancellationToken, codeIsError, suppressStdErr).ConfigureAwait(false); } /// /// Starts a commandline program and returns the contents of stdout /// /// /// The working directory to run in; null means current directory /// The cancellation token /// Callback method that is invoked with the error code from the process; the result indicates if the status code should be interpreted as an error. /// Default value is null which will treat anything non-zero as an error /// If true, stderr is not forwarded to the console /// Function to write to stdin /// The output from stdout public static async Task ExecuteWithOutput(IEnumerable command, string? workingDirectory = null, CancellationToken cancellationToken = default, Func? codeIsError = null, bool suppressStdErr = false, Func? writeStdIn = null) { if (!command.Any()) throw new ArgumentException("Needs at least one command", nameof(command)); workingDirectory ??= Environment.CurrentDirectory; if (!Directory.Exists(workingDirectory)) Directory.CreateDirectory(workingDirectory); codeIsError ??= (x) => x != 0; var p = Process.Start(new ProcessStartInfo(command.First(), command.Skip(1)) { WindowStyle = ProcessWindowStyle.Hidden, WorkingDirectory = workingDirectory, RedirectStandardError = !suppressStdErr, RedirectStandardOutput = true, RedirectStandardInput = writeStdIn != null, UseShellExecute = false, }) ?? throw new Exception($"Failed to launch process {command.First()}, null returned"); var tstdout = p.StandardOutput.ReadToEndAsync(cancellationToken); var tstderr = suppressStdErr ? Task.CompletedTask : p.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError(), cancellationToken); if (writeStdIn != null) await writeStdIn(p.StandardInput).ConfigureAwait(false); await p.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (codeIsError(p.ExitCode)) throw new Exception($"Execution of {command.First()} gave error code {p.ExitCode}"); await tstderr.ConfigureAwait(false); return await tstdout.ConfigureAwait(false); } /// /// Starts a commandline program and returns the contents of stdout /// /// /// The stream to write the output to /// The working directory to run in; null means current directory /// The cancellation token /// Callback method that is invoked with the error code from the process; the result indicates if the status code should be interpreted as an error. /// Default value is null which will treat anything non-zero as an error /// If true, stderr is not forwarded to the console /// Function to write to stdin /// The output from stdout public static async Task ExecuteWithOutput(IEnumerable command, Stream stdout, string? workingDirectory = null, CancellationToken cancellationToken = default, Func? codeIsError = null, bool suppressStdErr = false, Func? writeStdIn = null) { if (!command.Any()) throw new ArgumentException("Needs at least one command", nameof(command)); workingDirectory ??= Environment.CurrentDirectory; if (!Directory.Exists(workingDirectory)) Directory.CreateDirectory(workingDirectory); codeIsError ??= (x) => x != 0; var p = Process.Start(new ProcessStartInfo(command.First(), command.Skip(1)) { WindowStyle = ProcessWindowStyle.Hidden, WorkingDirectory = workingDirectory, RedirectStandardError = !suppressStdErr, RedirectStandardOutput = true, RedirectStandardInput = writeStdIn != null, UseShellExecute = false, }) ?? throw new Exception($"Failed to launch process {command.First()}, null returned"); var tstdout = p.StandardOutput.BaseStream.CopyToAsync(stdout, cancellationToken); var tstderr = suppressStdErr ? Task.CompletedTask : p.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError(), cancellationToken); if (writeStdIn != null) await writeStdIn(p.StandardInput).ConfigureAwait(false); await p.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (codeIsError(p.ExitCode)) throw new Exception($"Execution of {command.First()} gave error code {p.ExitCode}"); await tstderr.ConfigureAwait(false); await tstdout.ConfigureAwait(false); } /// /// Starts a commandline program and waits for it to complete /// /// /// The working directory to run in; null means current directory /// The cancellation token /// Callback method that is invoked with the error code from the process; the result indicates if the status code should be interpreted as an error. /// The folder where the log files are written /// Function to create custom filenames for the log files /// Default value is null which will treat anything non-zero as an error /// Function to write to stdin /// The output from stdout public static async Task ExecuteWithLog(IEnumerable command, string? workingDirectory = null, CancellationToken cancellationToken = default, Func? codeIsError = null, string? logFolder = null, Func? logFilename = null, Func? writeStdIn = null) { if (!command.Any()) throw new ArgumentException("Needs at least one command", nameof(command)); workingDirectory ??= Environment.CurrentDirectory; if (!Directory.Exists(workingDirectory)) Directory.CreateDirectory(workingDirectory); logFolder ??= workingDirectory; codeIsError ??= (x) => x != 0; var p = Process.Start(new ProcessStartInfo(command.First(), command.Skip(1)) { WindowStyle = ProcessWindowStyle.Hidden, WorkingDirectory = workingDirectory, RedirectStandardError = true, RedirectStandardOutput = true, RedirectStandardInput = writeStdIn != null, UseShellExecute = false, }) ?? throw new Exception($"Failed to launch process {command.First()}, null returned"); logFilename ??= (pid, isStdOut) => $"{command.First()}-{p.Id}.{(isStdOut ? "stdout" : "stderr")}.log"; using var logstdout = File.Create(Path.Combine(logFolder, logFilename(p.Id, true))); using var logstderr = File.Create(Path.Combine(logFolder, logFilename(p.Id, false))); var t1 = p.StandardOutput.BaseStream.CopyToAsync(logstdout, cancellationToken); var t2 = p.StandardError.BaseStream.CopyToAsync(logstderr, cancellationToken); if (writeStdIn != null) await writeStdIn(p.StandardInput).ConfigureAwait(false); await p.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (codeIsError(p.ExitCode)) throw new Exception($"Execution of {command.First()} gave error code {p.ExitCode}"); await t1.ConfigureAwait(false); await t2.ConfigureAwait(false); } }