#region Copyright 2009-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0
/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;
using System.IO;
using System.Text;
using CSharpTest.Net.Utils;
namespace CSharpTest.Net.Processes
{
///
/// Creates/Spawns a process with the standard error/out/in all mapped. Subscribe to
/// the OutputReceived event prior to start/run to listen to the program output, write
/// to the StandardInput for input.
///
public class ProcessRunner : IRunner
{
private static readonly string[] EmptyArgList = new string[0];
private readonly ManualResetEvent _mreProcessExit = new ManualResetEvent(true);
private readonly ManualResetEvent _mreOutputDone = new ManualResetEvent(true);
private readonly ManualResetEvent _mreErrorDone = new ManualResetEvent(true);
private readonly string _executable;
private readonly string[] _arguments;
private event ProcessOutputEventHandler _outputReceived;
private event ProcessExitedEventHandler _processExited;
private bool _isRunning;
private string _workingDir;
private volatile int _exitCode;
private Process _running;
private TextWriter _stdIn;
/// Creates a ProcessRunner for the given executable
public ProcessRunner(string executable)
: this(executable, EmptyArgList)
{ }
/// Creates a ProcessRunner for the given executable and arguments
public ProcessRunner(string executable, params string[] args)
{
_executable = Utils.FileUtils.FindFullPath(Check.NotEmpty(executable));
_arguments = args == null ? EmptyArgList : args;
_isRunning = false;
_exitCode = 0;
_running = null;
_stdIn = null;
}
/// Detaches event handlers and closes input streams
public void Dispose()
{
_outputReceived = null;
_processExited = null;
TextWriter w = _stdIn;
if(w != null) w.Dispose();
}
///
/// Returns the remote process Id
///
public int PID { get { return _running.Id; } }
/// Returns a debug-view string of process/arguments to execute
public override string ToString()
{
return String.Format("{0} {1}", _executable, ArgumentList.EscapeArguments(_arguments));
}
/// Notifies caller of writes to the std::err or std::out
public event ProcessOutputEventHandler OutputReceived
{
add { lock (this) _outputReceived += value; }
remove { lock (this) _outputReceived -= value; }
}
/// Notifies caller when the process exits
public event ProcessExitedEventHandler ProcessExited
{
add { lock (this) _processExited += value; }
remove { lock (this) _processExited -= value; }
}
/// Allows writes to the std::in for the process
public TextWriter StandardInput { get { return Check.NotNull(_stdIn); } }
/// Gets or sets the initial working directory for the process.
public string WorkingDirectory { get { return _workingDir ?? Environment.CurrentDirectory; } set { _workingDir = value; } }
/// Waits for the process to exit and returns the exit code
public int ExitCode { get { WaitForExit(); return _exitCode; } }
/// Kills the process if it is still running
public void Kill()
{
int attempt = 3;
try
{
while (attempt-- >= 0 && _isRunning && !WaitForExit(TimeSpan.Zero, false))
{
try
{
if (_running != null && !_running.HasExited)
_running.Kill();
}
catch (System.InvalidOperationException) { break; }
}
}
catch (System.ComponentModel.Win32Exception)
{ }
TryRaiseExitedEvent(_mreErrorDone);
TryRaiseExitedEvent(_mreOutputDone);
TryRaiseExitedEvent(_mreProcessExit);
_isRunning = false;
}
/// Closes std::in and waits for the process to exit
public void WaitForExit()
{
WaitForExit(TimeSpan.MaxValue, true);
}
/// Closes std::in and waits for the process to exit, returns false if the process did not exit in the time given
public bool WaitForExit(TimeSpan timeout) { return WaitForExit(timeout, true); }
/// Waits for the process to exit, returns false if the process did not exit in the time given
public bool WaitForExit(TimeSpan timeout, bool closeStdInput)
{
if (_stdIn != null && closeStdInput)
{ _stdIn.Close(); _stdIn = null; }
int waitTime = timeout.TotalMilliseconds >= int.MaxValue ? -1 : (int)timeout.TotalMilliseconds;
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
{
if (!_mreProcessExit.WaitOne(waitTime, false))
return false;
if (!_mreErrorDone.WaitOne(waitTime, false))
return false;
if (!_mreOutputDone.WaitOne(waitTime, false))
return false;
return true;
}
bool exited = WaitHandle.WaitAll(new WaitHandle[] { _mreErrorDone, _mreOutputDone, _mreProcessExit }, waitTime, false);
return exited;
}
/// Returns true if this instance is running a process
public bool IsRunning { get { return _isRunning && !WaitForExit(TimeSpan.Zero, false); } }
#region Run, Start, & Overloads
/// Runs the process and returns the exit code.
public int Run() { return Run(null, EmptyArgList); }
/// Runs the process with additional arguments and returns the exit code.
public int Run(params string[] moreArguments) { return Run(null, moreArguments); }
/// Runs the process with additional arguments and returns the exit code.
public int Run(TextReader input, params string[] arguments)
{
List args = new List(_arguments);
args.AddRange(arguments ?? EmptyArgList);
return InternalRun(input, args.ToArray());
}
///
/// Calls String.Format() for each argument this runner was constructed with giving the object
/// array as the arguments. Once complete it runs the process with the new set of arguments and
/// returns the exit code.
///
public int RunFormatArgs(params object[] formatArgs)
{
Check.NotNull(formatArgs);
List args = new List();
foreach (string arg in _arguments)
args.Add(String.Format(arg, formatArgs));
return InternalRun(null, args.ToArray());
}
/// Starts the process and returns.
public void Start() { Start(EmptyArgList); }
/// Starts the process with additional arguments and returns.
public void Start(params string[] moreArguments)
{
List args = new List(_arguments);
args.AddRange(moreArguments ?? EmptyArgList);
InternalStart(args.ToArray());
}
///
/// Calls String.Format() for each argument this runner was constructed with giving the object
/// array as the arguments. Once complete it starts the process with the new set of arguments and
/// returns.
///
public void StartFormatArgs(params object[] formatArgs)
{
Check.NotNull(formatArgs);
List args = new List();
foreach (string arg in _arguments)
args.Add(String.Format(arg, formatArgs));
InternalStart(args.ToArray());
}
#endregion Run, Start, & Overloads
private int InternalRun(TextReader input, string[] arguments)
{
InternalStart(arguments);
if (input != null)
{
char[] buffer = new char[1024];
int count;
while (0 != (count = input.Read(buffer, 0, buffer.Length)))
StandardInput.Write(buffer, 0, count);
}
WaitForExit();
return ExitCode;
}
private void InternalStart(params string[] arguments)
{
if (IsRunning)
throw new InvalidOperationException(Resources.ProcessRunnerAlreadyRunning);
_isRunning = true;
_mreProcessExit.Reset();
_mreOutputDone.Reset();
_mreErrorDone.Reset();
_exitCode = 0;
_stdIn = null;
_running = new Process();
string stringArgs = ArgumentList.EscapeArguments(arguments);
ProcessStartInfo psi = new ProcessStartInfo(_executable, stringArgs);
psi.WorkingDirectory = this.WorkingDirectory;
psi.RedirectStandardInput = true;
psi.RedirectStandardError = true;
psi.RedirectStandardOutput = true;
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
psi.ErrorDialog = false;
_running.StartInfo = psi;
_running.Exited += process_Exited;
_running.OutputDataReceived += process_OutputDataReceived;
_running.ErrorDataReceived += process_ErrorDataReceived;
_running.EnableRaisingEvents = true;
Trace.TraceInformation("EXEC: {0} {1}", _running.StartInfo.FileName, _running.StartInfo.Arguments);
_running.Start();
_stdIn = _running.StandardInput;
_running.BeginOutputReadLine();
_running.BeginErrorReadLine();
}
private void OnOutputReceived(ProcessOutputEventArgs args)
{
lock (this)
{
if (_outputReceived != null)
_outputReceived(this, args);
}
}
void TryRaiseExitedEvent(ManualResetEvent completing)
{
if (completing == null || completing.WaitOne(0, false))
return;//bad signal or already complete.
try
{
if (_processExited != null)
{
bool isComplete = false;
ProcessExitedEventHandler handler;
lock (this)
{
if (null != (handler = _processExited))
{
if ( //determine if we are 'about' to complete with this signal
(Object.ReferenceEquals(completing, _mreProcessExit) || _mreProcessExit.WaitOne(0, false)) &&
(Object.ReferenceEquals(completing, _mreOutputDone) || _mreOutputDone.WaitOne(0, false)) &&
(Object.ReferenceEquals(completing, _mreErrorDone) || _mreErrorDone.WaitOne(0, false))
)
{
isComplete = true;
}
}
}
if (isComplete)
{
_isRunning = false;
if (handler != null)
handler(this, new ProcessExitedEventArgs(_exitCode));
}
}
}
finally
{
completing.Set();
}
}
void process_Exited(object o, EventArgs e)
{
Trace.TraceInformation("EXIT: {0}", _running.StartInfo.FileName);
try { _exitCode = _running.ExitCode; }
catch (InvalidOperationException) { _exitCode = -1; }
TryRaiseExitedEvent(_mreProcessExit);
}
void process_OutputDataReceived(object o, DataReceivedEventArgs e)
{ InternalOutputDataReceived(e.Data); }
void InternalOutputDataReceived(string data)
{
if (data != null)
OnOutputReceived(new ProcessOutputEventArgs(data, false));
else
TryRaiseExitedEvent(_mreOutputDone);
}
void process_ErrorDataReceived(object o, DataReceivedEventArgs e)
{ InternalErrorDataReceived(e.Data); }
void InternalErrorDataReceived(string data)
{
if (data != null)
OnOutputReceived(new ProcessOutputEventArgs(data, true));
else
TryRaiseExitedEvent(_mreErrorDone);
}
}
}