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);
}
}