2015-10-06 10:46:43 -07:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
using System.Runtime.CompilerServices ;
2015-10-13 14:31:29 -07:00
using System.Runtime.InteropServices ;
2015-10-06 10:46:43 -07:00
using System.Threading.Tasks ;
namespace Microsoft.DotNet.Cli.Utils
{
public class Command
{
private Process _process ;
private StringWriter _stdOutCapture ;
private StringWriter _stdErrCapture ;
private TextWriter _stdOutForward ;
private TextWriter _stdErrForward ;
private Action < string > _stdOutHandler ;
private Action < string > _stdErrHandler ;
private bool _running = false ;
private Command ( string executable , string args )
{
// Set the things we need
var psi = new ProcessStartInfo ( )
{
FileName = executable ,
Arguments = args ,
RedirectStandardError = true ,
RedirectStandardOutput = true
} ;
_process = new Process ( )
{
StartInfo = psi
} ;
}
public static Command Create ( string executable , IEnumerable < string > args )
{
return Create ( executable , args . Any ( ) ? string . Join ( " " , args ) : string . Empty ) ;
}
public static Command Create ( string executable , string args )
{
2015-10-15 12:18:45 -07:00
// On Windows, we want to avoid using "cmd" if possible (it mangles the colors, and a bunch of other things)
// So, do a quick path search to see if we can just directly invoke it
var useCmd = ShouldUseCmd ( executable ) ;
if ( useCmd )
2015-10-06 10:46:43 -07:00
{
2015-10-15 12:18:45 -07:00
var comSpec = Environment . GetEnvironmentVariable ( "ComSpec" ) ;
2015-10-06 10:46:43 -07:00
// cmd doesn't like "foo.exe ", so we need to ensure that if
// args is empty, we just run "foo.exe"
if ( ! string . IsNullOrEmpty ( args ) )
{
2015-10-15 12:56:07 -07:00
executable = ( executable + " " + args ) . Replace ( "\"" , "\\\"" ) ;
2015-10-06 10:46:43 -07:00
}
2015-10-15 12:56:07 -07:00
args = $"/C \" { executable } \ "" ;
2015-10-06 10:46:43 -07:00
executable = comSpec ;
}
2015-10-15 12:18:45 -07:00
return new Command ( executable , args ) ;
}
private static bool ShouldUseCmd ( string executable )
{
if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
2015-10-06 10:46:43 -07:00
{
2015-10-15 12:18:45 -07:00
var extension = Path . GetExtension ( executable ) ;
2015-10-16 04:21:40 -07:00
if ( ! string . IsNullOrEmpty ( extension ) )
2015-10-15 12:18:45 -07:00
{
2015-10-16 04:21:40 -07:00
return ! string . Equals ( extension , ".exe" , StringComparison . Ordinal ) ;
2015-10-15 12:18:45 -07:00
}
else if ( executable . Contains ( Path . DirectorySeparatorChar ) )
{
// It's a relative path without an extension
if ( File . Exists ( executable + ".exe" ) )
{
// It refers to an exe!
return false ;
}
}
else
{
// Search the path to see if we can find it
foreach ( var path in Environment . GetEnvironmentVariable ( "PATH" ) . Split ( Path . PathSeparator ) )
{
var candidate = Path . Combine ( path , executable + ".exe" ) ;
if ( File . Exists ( candidate ) )
{
// We found an exe!
return false ;
}
}
}
// It's a non-exe :(
return true ;
2015-10-06 10:46:43 -07:00
}
2015-10-15 12:18:45 -07:00
// Non-windows never uses cmd
return false ;
2015-10-06 10:46:43 -07:00
}
2015-10-21 03:11:27 -07:00
public CommandResult Execute ( )
2015-10-06 10:46:43 -07:00
{
ThrowIfRunning ( ) ;
_running = true ;
_process . OutputDataReceived + = ( sender , args ) = >
2015-10-21 03:11:27 -07:00
{
2015-10-06 10:46:43 -07:00
ProcessData ( args . Data , _stdOutCapture , _stdOutForward , _stdOutHandler ) ;
2015-10-21 03:11:27 -07:00
} ;
2015-10-06 10:46:43 -07:00
_process . ErrorDataReceived + = ( sender , args ) = >
2015-10-21 03:11:27 -07:00
{
2015-10-06 10:46:43 -07:00
ProcessData ( args . Data , _stdErrCapture , _stdErrForward , _stdErrHandler ) ;
2015-10-21 03:11:27 -07:00
} ;
2015-10-06 10:46:43 -07:00
_process . EnableRaisingEvents = true ;
#if DEBUG
2015-10-18 21:24:26 -07:00
var sw = Stopwatch . StartNew ( ) ;
2015-10-21 03:11:27 -07:00
Reporter . Output . WriteLine ( $"> {FormatProcessInfo(_process.StartInfo)}" . White ( ) ) ;
2015-10-06 10:46:43 -07:00
#endif
_process . Start ( ) ;
_process . BeginOutputReadLine ( ) ;
_process . BeginErrorReadLine ( ) ;
2015-10-21 03:11:27 -07:00
_process . WaitForExit ( ) ;
var exitCode = _process . ExitCode ;
2015-10-06 10:46:43 -07:00
2015-10-18 19:02:09 -07:00
#if DEBUG
2015-10-22 05:31:24 -07:00
var message = $"< {FormatProcessInfo(_process.StartInfo)} exited with {exitCode} in {sw.ElapsedMilliseconds} ms." ;
2015-10-20 02:42:29 -07:00
if ( exitCode = = 0 )
{
Reporter . Output . WriteLine ( message . Green ( ) . Bold ( ) ) ;
}
else
{
Reporter . Output . WriteLine ( message . Red ( ) . Bold ( ) ) ;
}
2015-10-18 19:02:09 -07:00
#endif
2015-10-06 10:46:43 -07:00
return new CommandResult (
exitCode ,
_stdOutCapture ? . GetStringBuilder ( ) ? . ToString ( ) ,
_stdErrCapture ? . GetStringBuilder ( ) ? . ToString ( ) ) ;
}
public Command CaptureStdOut ( )
{
ThrowIfRunning ( ) ;
_stdOutCapture = new StringWriter ( ) ;
return this ;
}
public Command CaptureStdErr ( )
{
ThrowIfRunning ( ) ;
_stdErrCapture = new StringWriter ( ) ;
return this ;
}
public Command ForwardStdOut ( TextWriter to = null )
{
ThrowIfRunning ( ) ;
_stdOutForward = to ? ? Console . Out ;
return this ;
}
public Command ForwardStdErr ( TextWriter to = null )
{
ThrowIfRunning ( ) ;
_stdErrForward = to ? ? Console . Error ;
return this ;
}
public Command OnOutputLine ( Action < string > handler )
{
ThrowIfRunning ( ) ;
if ( _stdOutHandler ! = null )
{
throw new InvalidOperationException ( "Already handling stdout!" ) ;
}
_stdOutHandler = handler ;
return this ;
}
public Command OnErrorLine ( Action < string > handler )
{
ThrowIfRunning ( ) ;
if ( _stdErrHandler ! = null )
{
throw new InvalidOperationException ( "Already handling stderr!" ) ;
}
_stdErrHandler = handler ;
return this ;
}
2015-10-21 03:11:27 -07:00
private string FormatProcessInfo ( ProcessStartInfo info )
{
if ( string . IsNullOrWhiteSpace ( info . Arguments ) )
{
return info . FileName ;
}
return info . FileName + " " + info . Arguments ;
}
2015-10-06 10:46:43 -07:00
private void ThrowIfRunning ( [ CallerMemberName ] string memberName = null )
{
if ( _running )
{
throw new InvalidOperationException ( $"Unable to invoke {memberName} after the command has been run" ) ;
}
}
private void ProcessData ( string data , StringWriter capture , TextWriter forward , Action < string > handler )
{
if ( data = = null )
{
return ;
}
if ( capture ! = null )
{
capture . WriteLine ( data ) ;
}
if ( forward ! = null )
{
forward . WriteLine ( data ) ;
}
if ( handler ! = null )
{
handler ( data ) ;
}
}
}
}