namespace Unosquare.Swan.Components { using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; /// /// Provides methods to help create external processes, and efficiently capture the /// standard error and standard output streams. /// public static class ProcessRunner { /// /// Defines a delegate to handle binary data reception from the standard /// output or standard error streams from a process. /// /// The process data. /// The process. public delegate void ProcessDataReceivedCallback(byte[] processData, Process process); /// /// Runs the process asynchronously and if the exit code is 0, /// returns all of the standard output text. If the exit code is something other than 0 /// it returns the contents of standard error. /// This method is meant to be used for programs that output a relatively small amount of text. /// /// The filename. /// The cancellation token. /// The type of the result produced by this Task. public static Task GetProcessOutputAsync(string filename, CancellationToken ct = default) => GetProcessOutputAsync(filename, string.Empty, ct); /// /// Runs the process asynchronously and if the exit code is 0, /// returns all of the standard output text. If the exit code is something other than 0 /// it returns the contents of standard error. /// This method is meant to be used for programs that output a relatively small amount of text. /// /// The filename. /// The arguments. /// The cancellation token. /// The type of the result produced by this Task. /// /// The following code explains how to run an external process using the /// method. /// /// class Example /// { /// using System.Threading.Tasks; /// using Unosquare.Swan.Components; /// /// static async Task Main() /// { /// // execute a process and save its output /// var data = await ProcessRunner. /// GetProcessOutputAsync("dotnet", "--help"); /// /// // print the output /// data.WriteLine(); /// } /// } /// /// public static async Task GetProcessOutputAsync( string filename, string arguments, CancellationToken ct = default) { var result = await GetProcessResultAsync(filename, arguments, ct).ConfigureAwait(false); return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; } /// /// Gets the process output asynchronous. /// /// The filename. /// The arguments. /// The working directory. /// The cancellation token. /// /// The type of the result produced by this Task. /// public static async Task GetProcessOutputAsync( string filename, string arguments, string workingDirectory, CancellationToken ct = default) { var result = await GetProcessResultAsync(filename, arguments, workingDirectory, ct: ct).ConfigureAwait(false); return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; } /// /// Runs the process asynchronously and if the exit code is 0, /// returns all of the standard output text. If the exit code is something other than 0 /// it returns the contents of standard error. /// This method is meant to be used for programs that output a relatively small amount /// of text using a different encoder. /// /// The filename. /// The arguments. /// The encoding. /// The cancellation token. /// /// The type of the result produced by this Task. /// public static async Task GetProcessEncodedOutputAsync( string filename, string arguments = "", Encoding encoding = null, CancellationToken ct = default) { var result = await GetProcessResultAsync(filename, arguments, null, encoding, ct).ConfigureAwait(false); return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; } /// /// Executes a process asynchronously and returns the text of the standard output and standard error streams /// along with the exit code. This method is meant to be used for programs that output a relatively small /// amount of text. /// /// The filename. /// The arguments. /// The cancellation token. /// /// Text of the standard output and standard error streams along with the exit code as a instance. /// /// filename. public static Task GetProcessResultAsync( string filename, string arguments = "", CancellationToken ct = default) => GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, ct); /// /// Executes a process asynchronously and returns the text of the standard output and standard error streams /// along with the exit code. This method is meant to be used for programs that output a relatively small /// amount of text. /// /// The filename. /// The arguments. /// The working directory. /// The encoding. /// The cancellation token. /// /// Text of the standard output and standard error streams along with the exit code as a instance. /// /// filename. /// /// The following code describes how to run an external process using the method. /// /// class Example /// { /// using System.Threading.Tasks; /// using Unosquare.Swan.Components; /// static async Task Main() /// { /// // Execute a process asynchronously /// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help"); /// // print out the exit code /// $"{data.ExitCode}".WriteLine(); /// // print out the output /// data.StandardOutput.WriteLine(); /// // and the error if exists /// data.StandardError.Error(); /// } /// } /// public static async Task GetProcessResultAsync( string filename, string arguments, string workingDirectory, Encoding encoding = null, CancellationToken ct = default) { if (filename == null) throw new ArgumentNullException(nameof(filename)); if (encoding == null) encoding = Definitions.CurrentAnsiEncoding; var standardOutputBuilder = new StringBuilder(); var standardErrorBuilder = new StringBuilder(); var processReturn = await RunProcessAsync( filename, arguments, workingDirectory, (data, proc) => { standardOutputBuilder.Append(encoding.GetString(data)); }, (data, proc) => { standardErrorBuilder.Append(encoding.GetString(data)); }, encoding, true, ct) .ConfigureAwait(false); return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString()); } /// /// Runs an external process asynchronously, providing callbacks to /// capture binary data from the standard error and standard output streams. /// The callbacks contain a reference to the process so you can respond to output or /// error streams by writing to the process' input stream. /// The exit code (return value) will be -1 for forceful termination of the process. /// /// The filename. /// The arguments. /// The working directory. /// The on output data. /// The on error data. /// The encoding. /// if set to true the next data callback will wait until the current one completes. /// The cancellation token. /// /// Value type will be -1 for forceful termination of the process. /// public static Task RunProcessAsync( string filename, string arguments, string workingDirectory, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback onErrorData, Encoding encoding, bool syncEvents = true, CancellationToken ct = default) { if (filename == null) throw new ArgumentNullException(nameof(filename)); return Task.Run(() => { // Setup the process and its corresponding start info var process = new Process { EnableRaisingEvents = false, StartInfo = new ProcessStartInfo { Arguments = arguments, CreateNoWindow = true, FileName = filename, RedirectStandardError = true, StandardErrorEncoding = encoding, RedirectStandardOutput = true, StandardOutputEncoding = encoding, UseShellExecute = false, #if NET452 WindowStyle = ProcessWindowStyle.Hidden, #endif }, }; if (!string.IsNullOrWhiteSpace(workingDirectory)) process.StartInfo.WorkingDirectory = workingDirectory; // Launch the process and discard any buffered data for standard error and standard output process.Start(); process.StandardError.DiscardBufferedData(); process.StandardOutput.DiscardBufferedData(); // Launch the asynchronous stream reading tasks var readTasks = new Task[2]; readTasks[0] = CopyStreamAsync( process, process.StandardOutput.BaseStream, onOutputData, syncEvents, ct); readTasks[1] = CopyStreamAsync( process, process.StandardError.BaseStream, onErrorData, syncEvents, ct); try { // Wait for all tasks to complete Task.WaitAll(readTasks, ct); } catch (TaskCanceledException) { // ignore } finally { // Wait for the process to exit while (ct.IsCancellationRequested == false) { if (process.HasExited || process.WaitForExit(5)) break; } // Forcefully kill the process if it do not exit try { if (process.HasExited == false) process.Kill(); } catch { // swallow } } try { // Retrieve and return the exit code. // -1 signals error return process.HasExited ? process.ExitCode : -1; } catch { return -1; } }, ct); } /// /// Runs an external process asynchronously, providing callbacks to /// capture binary data from the standard error and standard output streams. /// The callbacks contain a reference to the process so you can respond to output or /// error streams by writing to the process' input stream. /// The exit code (return value) will be -1 for forceful termination of the process. /// /// The filename. /// The arguments. /// The on output data. /// The on error data. /// if set to true the next data callback will wait until the current one completes. /// The cancellation token. /// Value type will be -1 for forceful termination of the process. /// /// The following example illustrates how to run an external process using the /// /// method. /// /// class Example /// { /// using System.Diagnostics; /// using System.Text; /// using System.Threading.Tasks; /// using Unosquare.Swan; /// using Unosquare.Swan.Components; /// /// static async Task Main() /// { /// // Execute a process asynchronously /// var data = await ProcessRunner /// .RunProcessAsync("dotnet", "--help", Print, Print); /// /// // flush all messages /// Terminal.Flush(); /// } /// /// // a callback to print both output or errors /// static void Print(byte[] data, Process proc) => /// Encoding.GetEncoding(0).GetString(data).WriteLine(); /// } /// /// public static Task RunProcessAsync( string filename, string arguments, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback onErrorData, bool syncEvents = true, CancellationToken ct = default) => RunProcessAsync( filename, arguments, null, onOutputData, onErrorData, Definitions.CurrentAnsiEncoding, syncEvents, ct); /// /// Copies the stream asynchronously. /// /// The process. /// The source stream. /// The on data callback. /// if set to true [synchronize events]. /// The cancellation token. /// Total copies stream. private static Task CopyStreamAsync( Process process, Stream baseStream, ProcessDataReceivedCallback onDataCallback, bool syncEvents, CancellationToken ct) { return Task.Factory.StartNew(async () => { // define some state variables var swapBuffer = new byte[2048]; // the buffer to copy data from one stream to the next ulong totalCount = 0; // the total amount of bytes read var hasExited = false; while (ct.IsCancellationRequested == false) { try { // Check if process is no longer valid // if this condition holds, simply read the last bits of data available. int readCount; // the bytes read in any given event if (process.HasExited || process.WaitForExit(1)) { while (true) { try { readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct); if (readCount > 0) { totalCount += (ulong) readCount; onDataCallback?.Invoke(swapBuffer.Skip(0).Take(readCount).ToArray(), process); } else { hasExited = true; break; } } catch { hasExited = true; break; } } } if (hasExited) break; // Try reading from the stream. < 0 means no read occurred. readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct); // When no read is done, we need to let is rest for a bit if (readCount <= 0) { await Task.Delay(1, ct); // do not hog CPU cycles doing nothing. continue; } totalCount += (ulong) readCount; if (onDataCallback == null) continue; // Create the buffer to pass to the callback var eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray(); // Create the data processing callback invocation var eventTask = Task.Factory.StartNew(() => { onDataCallback.Invoke(eventBuffer, process); }, ct); // wait for the event to process before the next read occurs if (syncEvents) eventTask.Wait(ct); } catch { break; } } return totalCount; }, ct).Unwrap(); } } }