Created
December 17, 2020 13:46
-
-
Save ygoe/94ec60d80a760b189e8030bdd20691a4 to your computer and use it in GitHub Desktop.
ProcessHelper class: Provides methods for process execution and handling in C#. Because it's hard.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using DotforwardControl.Shared.Extensions; | |
namespace DotforwardControl.Shared.Util | |
{ | |
/// <summary> | |
/// Provides methods for process execution and handling. | |
/// </summary> | |
public static class ProcessHelper | |
{ | |
/// <summary> | |
/// Executes a process with a timeout and reads all output. | |
/// </summary> | |
/// <param name="fileName">The application to execute.</param> | |
/// <param name="args">Command-line arguments to use when starting the application.</param> | |
/// <param name="timeout">The time to wait for the process to exit. When this time elapses, the process is killed.</param> | |
/// <param name="stdin">Optional content to pass in to the process. If null, nothing is sent.</param> | |
/// <param name="cancellationToken">Indicates that the wait for the completion should be aborted.</param> | |
/// <returns>The process result.</returns> | |
public static Task<ProcessResult> Execute(string fileName, IEnumerable<string> args, TimeSpan timeout, string stdin = null, CancellationToken cancellationToken = default) | |
{ | |
var startInfo = new ProcessStartInfo | |
{ | |
FileName = fileName, | |
UseShellExecute = false, | |
CreateNoWindow = true | |
}; | |
foreach (string arg in args) | |
{ | |
startInfo.ArgumentList.Add(arg); | |
} | |
return Execute(startInfo, timeout, stdin, cancellationToken); | |
} | |
/// <summary> | |
/// Executes a process with a timeout and reads all output. | |
/// </summary> | |
/// <param name="startInfo">The process start information. Any stream redirection is enabled automatically.</param> | |
/// <param name="timeout">The time to wait for the process to exit. When this time elapses, the process is killed.</param> | |
/// <param name="stdin">Optional content to pass in to the process. If null, nothing is sent.</param> | |
/// <param name="cancellationToken">Indicates that the wait for the completion should be aborted.</param> | |
/// <returns>The process result.</returns> | |
public static async Task<ProcessResult> Execute(ProcessStartInfo startInfo, TimeSpan timeout, string stdin = null, CancellationToken cancellationToken = default) | |
{ | |
startInfo.RedirectStandardOutput = true; | |
startInfo.RedirectStandardError = true; | |
startInfo.RedirectStandardInput = stdin != null; | |
bool timedOut = false; | |
using var process = Process.Start(startInfo); | |
if (stdin != null) | |
{ | |
process.StandardInput.Write(stdin); | |
process.StandardInput.Close(); | |
} | |
// Read both streams asynchronously (in parallel) so that the process won't block on | |
// writing to them when we're not reading yet and the buffer becomes full. This | |
// ensures we're consuming the stream in a timely manner. | |
var stdoutTask = process.StandardOutput.ReadToEndAsync(); | |
var stderrTask = process.StandardError.ReadToEndAsync(); | |
// Wait for both streams to close, as an indication that the process should be | |
// completed. At least we know that we have all the output there is. If this doesn't | |
// happen in a certain time, the process is killed asynchronously, which should also | |
// close both streams sooner or later. Before accessing ExitCode, the process must | |
// really have exited, so we also wait for that (still covered by the timeout). | |
using (var timeoutCts = new CancellationTokenSource(timeout)) | |
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)) | |
using (cts.Token.Register(() => { try { process.Kill(); timedOut = true; } catch { } })) | |
{ | |
await Task.WhenAll(stdoutTask, stderrTask); | |
await process.WaitForExitAsync(cancellationToken); | |
} | |
return new ProcessResult(process.ExitCode, await stdoutTask, await stderrTask, timedOut); | |
} | |
/// <summary> | |
/// Returns a string that represents a single argument. If necessary, it is quoted. | |
/// </summary> | |
/// <param name="arg">The argument string.</param> | |
/// <returns>The quoted argument string.</returns> | |
public static string GetArgString(string arg) => | |
string.IsNullOrWhiteSpace(arg) || arg.Contains(' ') ? "\"" + arg + "\"" : arg; | |
/// <summary> | |
/// Returns a string that represents multiple arguments. If necessary, each is quoted. | |
/// </summary> | |
/// <param name="args">The arguments.</param> | |
/// <returns>The quoted arguments string.</returns> | |
public static string GetArgsString(IEnumerable<string> args) => | |
string.Join(' ', args.Select(a => GetArgString(a))); | |
} | |
/// <summary> | |
/// Contains data about an exited process. | |
/// </summary> | |
public class ProcessResult | |
{ | |
/// <summary> | |
/// Initializes a new instance of the <see cref="ProcessResult"/> class. | |
/// </summary> | |
/// <param name="exitCode">The process exit code.</param> | |
/// <param name="stdout">The contents of the standard output stream.</param> | |
/// <param name="stderr">The contents of the standard error stream.</param> | |
/// <param name="timedOut">A value indicating whether the process has timed out.</param> | |
public ProcessResult(int exitCode, string stdout, string stderr, bool timedOut) | |
{ | |
ExitCode = exitCode; | |
StandardOutput = stdout; | |
StandardError = stderr; | |
TimedOut = timedOut; | |
} | |
/// <summary>Gets the process exit code.</summary> | |
public int ExitCode { get; } | |
/// <summary>Gets the contents of the standard output stream.</summary> | |
public string StandardOutput { get; } | |
/// <summary>Gets the contents of the standard error stream.</summary> | |
public string StandardError { get; } | |
/// <summary>Gets a value indicating whether the process has timed out.</summary> | |
public bool TimedOut { get; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note to self: Possible alternative (yet unverified): https://github.com/Tyrrrz/CliWrap#piping