2019-05-07 14:40:59 +03:00
/ *
* Copyright 2014 Xamarin Inc . All rights reserved .
2020-06-18 13:34:07 +03:00
* Copyright 2019 , 2020 Microsoft Corp . All rights reserved .
2019-05-07 14:40:59 +03:00
*
* Authors :
* Rolf Bjarne Kvinge < rolf @xamarin . com >
*
* /
using System ;
2019-10-14 17:18:46 +03:00
using System.Collections.Generic ;
2019-05-07 14:40:59 +03:00
using System.Diagnostics ;
2020-06-18 13:34:07 +03:00
using System.IO ;
2019-05-07 14:40:59 +03:00
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
2022-05-10 16:23:54 +03:00
#nullable enable
2022-05-09 16:42:34 +03:00
2020-06-18 13:34:07 +03:00
namespace Xamarin.Utils {
public class Execution {
2022-05-10 16:23:54 +03:00
public string? FileName ;
public IList < string > ? Arguments ;
[tools] Fix nullability for the Execution.Environment field. (#15084)
The values for environment variables can be null (to remove said environment
variable).
Fixes this warning:
tests/dotnet/UnitTests/TestBaseClass.cs(294,100): warning CS8620: Argument of type 'Dictionary<string, string?>' cannot be used for parameter 'environment' of type 'Dictionary<string, string>' in 'Task<Execution> Execution.RunWithStringBuildersAsync(string filename, IList<string> arguments, Dictionary<string, string>? environment = null, StringBuilder? standardOutput = null, StringBuilder? standardError = null, TextWriter? log = null, string? workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)' due to differences in the nullability of reference types.
2022-05-23 18:36:06 +03:00
public IDictionary < string , string? > ? Environment ;
2022-05-10 16:23:54 +03:00
public string? WorkingDirectory ;
2020-06-18 13:34:07 +03:00
public TimeSpan ? Timeout ;
public CancellationToken ? CancellationToken ;
2019-10-14 17:18:46 +03:00
2022-05-10 16:23:54 +03:00
public TextWriter ? Log ;
2020-06-18 13:34:07 +03:00
public int ExitCode { get ; private set ; }
public bool TimedOut { get ; private set ; }
2022-05-10 16:23:54 +03:00
public TextWriter ? StandardOutput { get ; private set ; }
public TextWriter ? StandardError { get ; private set ; }
2020-06-18 13:34:07 +03:00
static Thread StartOutputThread ( TaskCompletionSource < Execution > tcs , object lockobj , StreamReader reader , TextWriter writer , string thread_name )
2019-10-14 17:18:46 +03:00
{
2020-06-18 13:34:07 +03:00
var thread = new Thread ( ( ) = > {
try {
2022-05-10 16:23:54 +03:00
while ( reader . ReadLine ( ) is string line ) {
2020-06-18 13:34:07 +03:00
lock ( lockobj )
writer . WriteLine ( line ) ;
}
} catch ( Exception e ) {
tcs . TrySetException ( e ) ;
} finally {
// The Process instance doesn't dispose these streams, which means we need to do it,
// otherwise we can run out of file descriptors while waiting for the GC to kick in.
// Ref: https://bugzilla.xamarin.com/show_bug.cgi?id=43462
reader . Dispose ( ) ;
}
} ) {
IsBackground = true ,
Name = thread_name ,
} ;
thread . Start ( ) ;
return thread ;
2019-05-07 14:40:59 +03:00
}
2020-06-18 13:34:07 +03:00
public Task < Execution > RunAsync ( )
2019-05-07 14:40:59 +03:00
{
2020-06-18 13:34:07 +03:00
var tcs = new TaskCompletionSource < Execution > ( ) ;
var lockobj = new object ( ) ;
try {
var p = new Process ( ) ;
p . StartInfo . FileName = FileName ;
2022-05-11 14:08:57 +03:00
p . StartInfo . Arguments = Arguments is not null ? StringUtils . FormatArguments ( Arguments ) : "" ;
2020-06-18 13:34:07 +03:00
p . StartInfo . UseShellExecute = false ;
p . StartInfo . RedirectStandardInput = false ;
p . StartInfo . RedirectStandardOutput = true ;
p . StartInfo . RedirectStandardError = true ;
if ( ! string . IsNullOrEmpty ( WorkingDirectory ) )
p . StartInfo . WorkingDirectory = WorkingDirectory ;
// mtouch/mmp writes UTF8 data outside of the ASCII range, so we need to make sure
// we read it in the same format. This also means we can't use the events to get
// stdout/stderr, because mono's Process class parses those using Encoding.Default.
p . StartInfo . StandardOutputEncoding = Encoding . UTF8 ;
p . StartInfo . StandardErrorEncoding = Encoding . UTF8 ;
if ( Environment ! = null ) {
foreach ( var kvp in Environment ) {
[tools] Fix nullability for the Execution.Environment field. (#15084)
The values for environment variables can be null (to remove said environment
variable).
Fixes this warning:
tests/dotnet/UnitTests/TestBaseClass.cs(294,100): warning CS8620: Argument of type 'Dictionary<string, string?>' cannot be used for parameter 'environment' of type 'Dictionary<string, string>' in 'Task<Execution> Execution.RunWithStringBuildersAsync(string filename, IList<string> arguments, Dictionary<string, string>? environment = null, StringBuilder? standardOutput = null, StringBuilder? standardError = null, TextWriter? log = null, string? workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)' due to differences in the nullability of reference types.
2022-05-23 18:36:06 +03:00
if ( kvp . Value is null ) {
2020-06-18 13:34:07 +03:00
p . StartInfo . EnvironmentVariables . Remove ( kvp . Key ) ;
} else {
p . StartInfo . EnvironmentVariables [ kvp . Key ] = kvp . Value ;
}
2019-05-07 14:40:59 +03:00
}
2020-06-18 13:34:07 +03:00
}
2019-05-07 14:40:59 +03:00
2020-06-18 13:34:07 +03:00
StandardOutput ? ? = new StringWriter ( ) ;
StandardError ? ? = new StringWriter ( ) ;
var thread = new Thread ( ( ) = > {
try {
if ( Log ! = null ) {
if ( ! string . IsNullOrEmpty ( p . StartInfo . WorkingDirectory ) )
2022-05-11 14:08:57 +03:00
Log . Write ( $"cd {StringUtils.Quote (p.StartInfo.WorkingDirectory!)} && " ) ;
2020-06-18 13:34:07 +03:00
Log . WriteLine ( "{0} {1}" , p . StartInfo . FileName , p . StartInfo . Arguments ) ;
}
p . Start ( ) ;
var pid = p . Id ;
var stdoutThread = StartOutputThread ( tcs , lockobj , p . StandardOutput , StandardOutput , $"StandardOutput reader for {p.StartInfo.FileName} (PID: {pid})" ) ;
var stderrThread = StartOutputThread ( tcs , lockobj , p . StandardError , StandardError , $"StandardError reader for {p.StartInfo.FileName} (PID: {pid})" ) ;
CancellationToken ? . Register ( ( ) = > {
// Don't call tcs.TrySetCanceled, that won't return an Execution result to the caller.
try {
p . Kill ( ) ;
} catch ( Exception ex ) {
// The process could be disposed already. Just ignore any exceptions here.
Log ? . WriteLine ( $"Failed to cancel and kill PID {pid}: {ex.Message}" ) ;
}
} ) ;
if ( Timeout . HasValue ) {
if ( ! p . WaitForExit ( ( int ) Timeout . Value . TotalMilliseconds ) ) {
2021-06-24 09:39:22 +03:00
Log ? . WriteLine ( $"Command '{p.StartInfo.FileName} {p.StartInfo.Arguments}' didn't finish in {Timeout.Value.TotalMilliseconds} ms, and will be killed." ) ;
2020-06-18 13:34:07 +03:00
TimedOut = true ;
try {
p . Kill ( ) ;
} catch ( Exception ex ) {
// According to the documentation, there can be exceptions here we can't prepare for, so just ignore them.
Log ? . WriteLine ( $"Failed to kill PID {pid}: {ex.Message}" ) ;
}
}
}
// Always call this WaitForExit overload to be make sure the stdout/stderr buffers have been flushed,
// even if we've called the WaitForExit (int) overload
p . WaitForExit ( ) ;
ExitCode = p . ExitCode ;
stdoutThread . Join ( TimeSpan . FromSeconds ( 1 ) ) ;
stderrThread . Join ( TimeSpan . FromSeconds ( 1 ) ) ;
tcs . TrySetResult ( this ) ;
} catch ( Exception e ) {
tcs . TrySetException ( e ) ;
} finally {
p . Dispose ( ) ;
2019-05-07 14:40:59 +03:00
}
2020-06-18 13:34:07 +03:00
} ) {
IsBackground = true ,
Name = $"Thread waiting for {p.StartInfo.FileName} to finish" ,
2019-05-07 14:40:59 +03:00
} ;
2020-06-18 13:34:07 +03:00
thread . Start ( ) ;
} catch ( Exception e ) {
tcs . TrySetException ( e ) ;
}
2019-05-07 14:40:59 +03:00
2020-06-18 13:34:07 +03:00
return tcs . Task ;
}
2019-05-07 14:40:59 +03:00
[tools] Fix nullability for the Execution.Environment field. (#15084)
The values for environment variables can be null (to remove said environment
variable).
Fixes this warning:
tests/dotnet/UnitTests/TestBaseClass.cs(294,100): warning CS8620: Argument of type 'Dictionary<string, string?>' cannot be used for parameter 'environment' of type 'Dictionary<string, string>' in 'Task<Execution> Execution.RunWithStringBuildersAsync(string filename, IList<string> arguments, Dictionary<string, string>? environment = null, StringBuilder? standardOutput = null, StringBuilder? standardError = null, TextWriter? log = null, string? workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)' due to differences in the nullability of reference types.
2022-05-23 18:36:06 +03:00
public static Task < Execution > RunWithCallbacksAsync ( string filename , IList < string > arguments , Dictionary < string , string? > ? environment = null , Action < string > ? standardOutput = null , Action < string > ? standardError = null , TextWriter ? log = null , string? workingDirectory = null , TimeSpan ? timeout = null , CancellationToken ? cancellationToken = null )
2020-06-18 13:34:07 +03:00
{
2022-05-10 16:23:54 +03:00
CallbackWriter ? outputCallback = null ;
CallbackWriter ? errorCallback = null ;
if ( standardOutput is not null )
2020-06-18 13:34:07 +03:00
outputCallback = new CallbackWriter { Callback = standardOutput } ;
if ( standardOutput = = standardError )
errorCallback = outputCallback ;
2022-05-10 16:23:54 +03:00
else if ( standardError is not null )
2020-06-18 13:34:07 +03:00
errorCallback = new CallbackWriter { Callback = standardError } ;
return RunAsync ( filename , arguments , environment , outputCallback , errorCallback , log , workingDirectory , timeout , cancellationToken ) ;
}
2019-05-07 14:40:59 +03:00
[tools] Fix nullability for the Execution.Environment field. (#15084)
The values for environment variables can be null (to remove said environment
variable).
Fixes this warning:
tests/dotnet/UnitTests/TestBaseClass.cs(294,100): warning CS8620: Argument of type 'Dictionary<string, string?>' cannot be used for parameter 'environment' of type 'Dictionary<string, string>' in 'Task<Execution> Execution.RunWithStringBuildersAsync(string filename, IList<string> arguments, Dictionary<string, string>? environment = null, StringBuilder? standardOutput = null, StringBuilder? standardError = null, TextWriter? log = null, string? workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)' due to differences in the nullability of reference types.
2022-05-23 18:36:06 +03:00
public static Task < Execution > RunAsync ( string filename , IList < string > arguments , Dictionary < string , string? > ? environment = null , TextWriter ? standardOutput = null , TextWriter ? standardError = null , TextWriter ? log = null , string? workingDirectory = null , TimeSpan ? timeout = null , CancellationToken ? cancellationToken = null )
2020-06-18 13:34:07 +03:00
{
return new Execution {
FileName = filename ,
Arguments = arguments ,
Environment = environment ,
StandardOutput = standardOutput ,
StandardError = standardError ,
WorkingDirectory = workingDirectory ,
CancellationToken = cancellationToken ,
Timeout = timeout ,
Log = log ,
} . RunAsync ( ) ;
}
2019-05-07 14:40:59 +03:00
[tools] Fix nullability for the Execution.Environment field. (#15084)
The values for environment variables can be null (to remove said environment
variable).
Fixes this warning:
tests/dotnet/UnitTests/TestBaseClass.cs(294,100): warning CS8620: Argument of type 'Dictionary<string, string?>' cannot be used for parameter 'environment' of type 'Dictionary<string, string>' in 'Task<Execution> Execution.RunWithStringBuildersAsync(string filename, IList<string> arguments, Dictionary<string, string>? environment = null, StringBuilder? standardOutput = null, StringBuilder? standardError = null, TextWriter? log = null, string? workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)' due to differences in the nullability of reference types.
2022-05-23 18:36:06 +03:00
public static Task < Execution > RunAsync ( string filename , IList < string > arguments , Dictionary < string , string? > ? environment = null , bool mergeOutput = false , string? workingDirectory = null , TextWriter ? log = null , TimeSpan ? timeout = null , CancellationToken ? cancellationToken = null )
2020-06-18 13:34:07 +03:00
{
var standardOutput = new StringWriter ( ) ;
var standardError = mergeOutput ? standardOutput : new StringWriter ( ) ;
return RunAsync ( filename , arguments , environment , standardOutput , standardError , log , workingDirectory , timeout , cancellationToken ) ;
2019-05-07 14:40:59 +03:00
}
[tools] Fix nullability for the Execution.Environment field. (#15084)
The values for environment variables can be null (to remove said environment
variable).
Fixes this warning:
tests/dotnet/UnitTests/TestBaseClass.cs(294,100): warning CS8620: Argument of type 'Dictionary<string, string?>' cannot be used for parameter 'environment' of type 'Dictionary<string, string>' in 'Task<Execution> Execution.RunWithStringBuildersAsync(string filename, IList<string> arguments, Dictionary<string, string>? environment = null, StringBuilder? standardOutput = null, StringBuilder? standardError = null, TextWriter? log = null, string? workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)' due to differences in the nullability of reference types.
2022-05-23 18:36:06 +03:00
public static Task < Execution > RunWithStringBuildersAsync ( string filename , IList < string > arguments , Dictionary < string , string? > ? environment = null , StringBuilder ? standardOutput = null , StringBuilder ? standardError = null , TextWriter ? log = null , string? workingDirectory = null , TimeSpan ? timeout = null , CancellationToken ? cancellationToken = null )
2019-05-07 14:40:59 +03:00
{
2020-06-18 13:34:07 +03:00
var stdout = standardOutput = = null ? null : new StringWriter ( standardOutput ) ;
var stderr = standardError = = null ? null : ( standardOutput = = standardError ? stdout : new StringWriter ( standardError ) ) ;
return RunAsync ( filename , arguments , environment , stdout , stderr , log , workingDirectory , timeout , cancellationToken ) ;
}
class CallbackWriter : TextWriter {
2022-05-10 16:23:54 +03:00
public Action < string > ? Callback ;
public override void WriteLine ( string? value )
2020-06-18 13:34:07 +03:00
{
2022-05-10 16:23:54 +03:00
if ( value is not null )
Callback ? . Invoke ( value ) ;
2020-06-18 13:34:07 +03:00
}
public override Encoding Encoding = > Encoding . UTF8 ;
2019-05-07 14:40:59 +03:00
}
}
}