using System; using System.Text; using System.Threading; using System.Diagnostics; using System.IO; using Microsoft.Win32.SafeHandles; namespace Common { public enum ProcessExecutorResult { None, Finished, Killed, OutputOverflow, TimedOut, Unexpected } public class ProcessExecutor : IDisposable { private readonly StringBuilder error; private readonly StringBuilder output; private bool disposedValue; private AutoResetEvent outputOverflowWaitHandle; private readonly object internalLock; private AsyncStreamReaderFast asyncStreamReaderOutput; private AsyncStreamReaderFast asyncStreamReaderError; public ManualResetEvent KillerWaitHandle; public ProcessStartInfo StartInfo { get; } public Process Process { get; private set; } public int TimeOut { get; set; } public int MaxOutputCount { get; set; } public delegate void OutputChangedHandler(ProcessExecutor processExecutor); public event OutputChangedHandler OutputChanged; public event OutputChangedHandler ErrorChanged; public bool UseAsyncStreamReaderFast; public ProcessExecutorResult Result; public string Error { get { lock (internalLock) { return error.ToString(); } } set { lock (internalLock) { error.Length = 0; error.Append(value); } } } public string Output { get { lock (internalLock) { return output.ToString(); } } set { lock (internalLock) { output.Length = 0; output.Append(value); } } } public ProcessExecutor() { UseAsyncStreamReaderFast = true; internalLock = new object(); Process = new Process(); StartInfo = new ProcessStartInfo { CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; Process.StartInfo = StartInfo; output = new StringBuilder(); error = new StringBuilder(); TimeOut = -1; MaxOutputCount = -1; KillerWaitHandle = null; outputOverflowWaitHandle = new AutoResetEvent(false); } public void Execute() { Result = ProcessExecutorResult.None; if (!File.Exists(Process.StartInfo.FileName)) { throw new Exception("Executable file " + Process.StartInfo.FileName + " not found."); } if (!UseAsyncStreamReaderFast) { Process.OutputDataReceived += OutputDataReceived; Process.ErrorDataReceived += ErrorDataReceived; } Process.Start(); if (UseAsyncStreamReaderFast) { asyncStreamReaderOutput = new AsyncStreamReaderFast(Process, Process.StandardOutput.BaseStream, OutputDataReceived, Process.StandardOutput.CurrentEncoding); asyncStreamReaderOutput.BeginRead(); asyncStreamReaderError = new AsyncStreamReaderFast(Process, Process.StandardError.BaseStream, ErrorDataReceived, Process.StandardOutput.CurrentEncoding); asyncStreamReaderError.BeginRead(); } else { Process.BeginOutputReadLine(); Process.BeginErrorReadLine(); } // From the Process.WaitForExit(int) documentation on the MSDN: // "When standard output has been redirected to asynchronous event handlers, // it is possible that output processing will not have completed when this method returns. // To ensure that asynchronous event handling has been completed, call the WaitForExit() // overload that takes no parameter after receiving a true from this overload." const int waitFinished = 0; const int waitKilled = 1; const int waitOutputOverflow = 2; int wait; using (SafeWaitHandle processHandle = new SafeWaitHandle(Process.Handle, false)) using (ManualResetEvent processFinishedEvent = new ManualResetEvent(false)) { processFinishedEvent.SafeWaitHandle = processHandle; WaitHandle[] waitHandles = { processFinishedEvent, KillerWaitHandle, outputOverflowWaitHandle }; if (TimeOut == -1) { wait = WaitHandle.WaitAny(waitHandles); } else { wait = WaitHandle.WaitAny(waitHandles, TimeOut * 1000); } } if (wait == waitFinished) { // see above if (UseAsyncStreamReaderFast) { asyncStreamReaderOutput.WaitUtilEOF(); asyncStreamReaderError.WaitUtilEOF(); } else { Process.WaitForExit(); } } else // Timed out or scheduled for execution. { try { Process.Kill(); } catch (System.ComponentModel.Win32Exception ex) { const int errorAccessDenied = 5; if (ex.NativeErrorCode != errorAccessDenied) throw; } catch (InvalidOperationException) { // Process has already exited. } } switch (wait) { case WaitHandle.WaitTimeout: Result = ProcessExecutorResult.TimedOut; break; case waitKilled: Result = ProcessExecutorResult.Killed; break; case waitOutputOverflow: Result = ProcessExecutorResult.OutputOverflow; break; case waitFinished: Result = ProcessExecutorResult.Finished; break; default: Result = ProcessExecutorResult.Unexpected; break; } } private void OutputDataReceived(string data) { if (string.IsNullOrEmpty(data)) return; lock (internalLock) { if (MaxOutputCount != -1) { if (output.Length + data.Length <= MaxOutputCount) { output.Append(data); } else { if (output.Length <= MaxOutputCount) { output.Append(data.Substring(0, MaxOutputCount - output.Length)); } else { outputOverflowWaitHandle.Set(); } } } else { output.Append(data); } } OutputChanged?.Invoke(this); } private void ErrorDataReceived(string data) { if (string.IsNullOrEmpty(data)) return; lock (internalLock) { if (MaxOutputCount != -1) { if (error.Length + data.Length <= MaxOutputCount) { error.Append(data); } else { if (error.Length <= MaxOutputCount) { error.Append(data.Substring(0, MaxOutputCount - error.Length)); } else { outputOverflowWaitHandle.Set(); } } } else { error.Append(data); } } ErrorChanged?.Invoke(this); } private void OutputDataReceived(object sender, DataReceivedEventArgs e) { OutputDataReceived(e.Data + "\n"); } private void ErrorDataReceived(object sender, DataReceivedEventArgs e) { ErrorDataReceived(e.Data + "\n"); } protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { lock (internalLock) { // Explicitly clear StringBuilder instances to avoid Out of memory exception output.Clear(); output.Capacity = 0; error.Clear(); error.Capacity = 0; } if (Process != null) { Process.Dispose(); Process = null; } if (outputOverflowWaitHandle != null) { outputOverflowWaitHandle.Close(); outputOverflowWaitHandle = null; } } disposedValue = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } ~ProcessExecutor() => Dispose(false); } }