#nullable enable using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Swan { /// /// 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. // [Obsolete("NEED", false)] 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 arguments. /// The working directory. /// 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 Swan; /// /// static async Task Main() /// { /// // execute a process and save its output /// var data = await ProcessRunner. /// GetProcessOutputAsync("dotnet", "--help"); /// /// // print the output /// data.WriteLine(); /// } /// } /// /// // [Obsolete("NEED", false)] public static async Task GetProcessOutputAsync(String filename, String arguments = "", String? workingDirectory = null, CancellationToken cancellationToken = default) { ProcessResult result = await GetProcessResultAsync(filename, arguments, workingDirectory, cancellationToken: cancellationToken).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. // [Obsolete("NEED", false)] public static Task GetProcessResultAsync(String filename, String arguments = "", CancellationToken cancellationToken = default) => GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, cancellationToken); /// /// 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 Swan; /// /// 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(); /// } /// } /// // [Obsolete("NEED", false)] public static async Task GetProcessResultAsync(String filename, String arguments, String? workingDirectory, Encoding? encoding = null, CancellationToken cancellationToken = default) { if(filename == null) { throw new ArgumentNullException(nameof(filename)); } if(encoding == null) { encoding = Definitions.CurrentAnsiEncoding; } StringBuilder standardOutputBuilder = new StringBuilder(); StringBuilder standardErrorBuilder = new StringBuilder(); Int32 processReturn = await RunProcessAsync(filename, arguments, workingDirectory, (data, proc) => standardOutputBuilder.Append(encoding.GetString(data)), (data, proc) => standardErrorBuilder.Append(encoding.GetString(data)), encoding, true, cancellationToken).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. /// // [Obsolete("NEED", false)] public static Task RunProcessAsync(String filename, String arguments, String? workingDirectory, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Encoding encoding, Boolean syncEvents = true, CancellationToken cancellationToken = default) { if(filename == null) { throw new ArgumentNullException(nameof(filename)); } return Task.Run(() => { // Setup the process and its corresponding start info Process 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(!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 Task[] readTasks = new Task[2]; readTasks[0] = CopyStreamAsync(process, process.StandardOutput.BaseStream, onOutputData, syncEvents, cancellationToken); readTasks[1] = CopyStreamAsync(process, process.StandardError.BaseStream, onErrorData, syncEvents, cancellationToken); try { // Wait for all tasks to complete Task.WaitAll(readTasks, cancellationToken); } catch(TaskCanceledException) { // ignore } finally { // Wait for the process to exit while(cancellationToken.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; } }, cancellationToken); } /// /// 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 Swan; /// /// 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(); /// } /// /// // [Obsolete("NEED", false)] public static Task RunProcessAsync(String filename, String arguments, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Boolean syncEvents = true, CancellationToken cancellationToken = default) => RunProcessAsync(filename, arguments, null, onOutputData, onErrorData, Definitions.CurrentAnsiEncoding, syncEvents, cancellationToken); /// /// 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. // [Obsolete("NEED", false)] private static Task CopyStreamAsync(Process process, Stream baseStream, ProcessDataReceivedCallback? onDataCallback, Boolean syncEvents, CancellationToken ct) => Task.Run(async () => { // define some state variables Byte[] swapBuffer = new Byte[2048]; // the buffer to copy data from one stream to the next UInt64 totalCount = 0; // the total amount of bytes read Boolean 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. Int32 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 += (UInt64)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).ConfigureAwait(false); // When no read is done, we need to let is rest for a bit if(readCount <= 0) { await Task.Delay(1, ct).ConfigureAwait(false); // do not hog CPU cycles doing nothing. continue; } totalCount += (UInt64)readCount; if(onDataCallback == null) { continue; } // Create the buffer to pass to the callback Byte[] eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray(); // Create the data processing callback invocation Task eventTask = Task.Run(() => 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); } }