2024-03-20 16:17:17 +01:00
using System.Diagnostics ;
namespace ReleaseBuilder ;
/// <summary>
/// Helper methods for executing a commandline program
/// </summary>
public static class ProcessHelper
{
/// <summary>
/// Starts a commandline program and waits for it to complete
/// </summary>
/// <param name="command"></param>
/// <param name="workingDirectory">The working directory to run in; <c>null</c> means current directory</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <param name="codeIsError">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 <c>null</c> which will treat anything non-zero as an error</param>
2024-03-22 15:20:45 +01:00
/// <param name="suppressStdErr">If <c>true</c>, stderr is not forwarded to the console</param>
2024-04-04 15:44:28 +02:00
/// <param name="writeStdIn">Function to write to stdin</param>
2024-03-20 16:17:17 +01:00
/// <returns>An awaitable task</returns>
2024-04-04 15:44:28 +02:00
public static async Task Execute ( IEnumerable < string > command , string? workingDirectory = null , CancellationToken cancellationToken = default , Func < int , bool >? codeIsError = null , bool suppressStdErr = false , Func < StreamWriter , Task >? writeStdIn = null )
2024-03-20 16:17:17 +01:00
{
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 ,
2024-03-22 15:20:45 +01:00
RedirectStandardError = ! suppressStdErr ,
2024-03-20 16:17:17 +01:00
RedirectStandardOutput = false ,
2024-04-04 15:44:28 +02:00
RedirectStandardInput = writeStdIn != null ,
2024-03-20 16:17:17 +01:00
UseShellExecute = false ,
}) ?? throw new Exception ( $"Failed to launch process {command.First()}, null returned" );
2024-03-22 12:15:53 +01:00
// Forward error messages to stderr
2024-03-22 15:20:45 +01:00
var t = suppressStdErr
? Task . CompletedTask
: p . StandardError . BaseStream . CopyToAsync ( Console . OpenStandardError (), cancellationToken );
2024-03-22 12:15:53 +01:00
2024-04-04 15:44:28 +02:00
if ( writeStdIn != null )
await writeStdIn ( p . StandardInput ). ConfigureAwait ( false );
2024-03-20 16:17:17 +01:00
await p . WaitForExitAsync ( cancellationToken ). ConfigureAwait ( false );
if ( codeIsError ( p . ExitCode ))
throw new Exception ( $"Execution of {command.First()} gave error code {p.ExitCode}" );
2024-03-22 12:15:53 +01:00
await t . ConfigureAwait ( false );
}
/// <summary>
/// Runs all commandline tasks in sequence
/// </summary>
/// <param name="commands">The commands to run</param>
/// <param name="workingDirectory">The working directory to run in; <c>null</c> means current directory</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <param name="codeIsError">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 <c>null</c> which will treat anything non-zero as an error</param>
2024-03-22 15:20:45 +01:00
/// <param name="suppressStdErr">If <c>true</c>, stderr is not forwarded to the console</param>
2024-03-22 12:15:53 +01:00
/// <returns>An awaitable task</returns>
2024-03-22 15:20:45 +01:00
public static async Task ExecuteAll ( IEnumerable < IEnumerable < string >> commands , string? workingDirectory = null , CancellationToken cancellationToken = default , Func < int , bool >? codeIsError = null , bool suppressStdErr = false )
2024-03-22 12:15:53 +01:00
{
foreach ( var c in commands )
2024-03-22 15:20:45 +01:00
await Execute ( c , workingDirectory , cancellationToken , codeIsError , suppressStdErr ). ConfigureAwait ( false );
2024-03-20 16:17:17 +01:00
}
/// <summary>
/// Starts a commandline program and returns the contents of stdout
/// </summary>
/// <param name="command"></param>
/// <param name="workingDirectory">The working directory to run in; <c>null</c> means current directory</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <param name="codeIsError">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 <c>null</c> which will treat anything non-zero as an error</param>
2024-03-22 15:20:45 +01:00
/// <param name="suppressStdErr">If <c>true</c>, stderr is not forwarded to the console</param>
2024-04-04 15:44:28 +02:00
/// <param name="writeStdIn">Function to write to stdin</param>
2024-03-20 16:17:17 +01:00
/// <returns>The output from stdout</returns>
2024-04-04 15:44:28 +02:00
public static async Task < string > ExecuteWithOutput ( IEnumerable < string > command , string? workingDirectory = null , CancellationToken cancellationToken = default , Func < int , bool >? codeIsError = null , bool suppressStdErr = false , Func < StreamWriter , Task >? writeStdIn = null )
2024-03-20 16:17:17 +01:00
{
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 ,
2024-03-22 15:20:45 +01:00
RedirectStandardError = ! suppressStdErr ,
2024-03-22 12:15:53 +01:00
RedirectStandardOutput = true ,
2024-04-04 15:44:28 +02:00
RedirectStandardInput = writeStdIn != null ,
2024-03-22 12:15:53 +01:00
UseShellExecute = false ,
}) ?? throw new Exception ( $"Failed to launch process {command.First()}, null returned" );
var tstdout = p . StandardOutput . ReadToEndAsync ( cancellationToken );
2024-03-22 15:20:45 +01:00
var tstderr = suppressStdErr
? Task . CompletedTask
: p . StandardError . BaseStream . CopyToAsync ( Console . OpenStandardError (), cancellationToken );
2024-03-22 12:15:53 +01:00
2024-04-04 15:44:28 +02:00
if ( writeStdIn != null )
await writeStdIn ( p . StandardInput ). ConfigureAwait ( false );
2024-03-22 12:15:53 +01:00
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 );
}
/// <summary>
/// Starts a commandline program and returns the contents of stdout
/// </summary>
/// <param name="command"></param>
/// <param name="stdout">The stream to write the output to</param>
/// <param name="workingDirectory">The working directory to run in; <c>null</c> means current directory</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <param name="codeIsError">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 <c>null</c> which will treat anything non-zero as an error</param>
2024-03-22 15:20:45 +01:00
/// <param name="suppressStdErr">If <c>true</c>, stderr is not forwarded to the console</param>
2024-04-04 15:44:28 +02:00
/// <param name="writeStdIn">Function to write to stdin</param>
2024-03-22 12:15:53 +01:00
/// <returns>The output from stdout</returns>
2024-04-04 15:44:28 +02:00
public static async Task ExecuteWithOutput ( IEnumerable < string > command , Stream stdout , string? workingDirectory = null , CancellationToken cancellationToken = default , Func < int , bool >? codeIsError = null , bool suppressStdErr = false , Func < StreamWriter , Task >? writeStdIn = null )
2024-03-22 12:15:53 +01:00
{
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 ,
2024-03-22 15:20:45 +01:00
RedirectStandardError = ! suppressStdErr ,
2024-03-20 16:17:17 +01:00
RedirectStandardOutput = true ,
2024-04-04 15:44:28 +02:00
RedirectStandardInput = writeStdIn != null ,
2024-03-20 16:17:17 +01:00
UseShellExecute = false ,
}) ?? throw new Exception ( $"Failed to launch process {command.First()}, null returned" );
2024-03-22 12:15:53 +01:00
var tstdout = p . StandardOutput . BaseStream . CopyToAsync ( stdout , cancellationToken );
2024-03-22 15:20:45 +01:00
var tstderr = suppressStdErr
? Task . CompletedTask
: p . StandardError . BaseStream . CopyToAsync ( Console . OpenStandardError (), cancellationToken );
2024-03-20 16:17:17 +01:00
2024-04-04 15:44:28 +02:00
if ( writeStdIn != null )
await writeStdIn ( p . StandardInput ). ConfigureAwait ( false );
2024-03-20 16:17:17 +01:00
await p . WaitForExitAsync ( cancellationToken ). ConfigureAwait ( false );
if ( codeIsError ( p . ExitCode ))
throw new Exception ( $"Execution of {command.First()} gave error code {p.ExitCode}" );
2024-03-22 12:15:53 +01:00
await tstderr . ConfigureAwait ( false );
await tstdout . ConfigureAwait ( false );
2024-03-20 16:17:17 +01:00
}
/// <summary>
/// Starts a commandline program and waits for it to complete
/// </summary>
/// <param name="command"></param>
/// <param name="workingDirectory">The working directory to run in; <c>null</c> means current directory</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <param name="codeIsError">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.
/// <param name="logFolder"/>The folder where the log files are written</param>
/// <param name="logFilename">Function to create custom filenames for the log files</param>
/// Default value is <c>null</c> which will treat anything non-zero as an error</param>
2024-04-04 15:44:28 +02:00
/// <param name="writeStdIn">Function to write to stdin</param>
2024-03-20 16:17:17 +01:00
/// <returns>The output from stdout</returns>
2024-04-04 15:44:28 +02:00
public static async Task ExecuteWithLog ( IEnumerable < string > command , string? workingDirectory = null , CancellationToken cancellationToken = default , Func < int , bool >? codeIsError = null , string? logFolder = null , Func < int , bool , string >? logFilename = null , Func < StreamWriter , Task >? writeStdIn = null )
2024-03-20 16:17:17 +01:00
{
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 ,
2024-04-04 15:44:28 +02:00
RedirectStandardInput = writeStdIn != null ,
2024-03-20 16:17:17 +01:00
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 );
2024-04-04 15:44:28 +02:00
if ( writeStdIn != null )
await writeStdIn ( p . StandardInput ). ConfigureAwait ( false );
2024-03-20 16:17:17 +01:00
await p . WaitForExitAsync ( cancellationToken ). ConfigureAwait ( false );
if ( codeIsError ( p . ExitCode ))
throw new Exception ( $"Execution of {command.First()} gave error code {p.ExitCode}" );
2024-03-22 12:15:53 +01:00
await t1 . ConfigureAwait ( false );
await t2 . ConfigureAwait ( false );
2024-03-20 16:17:17 +01:00
}
}