Skip to content

Instantly share code, notes, and snippets.

@Legends
Last active October 10, 2019 08:30
Show Gist options
  • Select an option

  • Save Legends/a9a42a30a2ec1dbb76bb3f989f5a18ed to your computer and use it in GitHub Desktop.

Select an option

Save Legends/a9a42a30a2ec1dbb76bb3f989f5a18ed to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace RunProcessAsTask
{
public static partial class ProcessEx
{
public static async Task<ProcessResults> RunAsync(ProcessStartInfo processStartInfo, List<string> standardOutput, List<string> standardError, CancellationToken cancellationToken)
{
// force some settings in the start info so we can capture the output
processStartInfo.UseShellExecute = false;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
var tcs = new TaskCompletionSource<ProcessResults>();
var process = new Process {
StartInfo = processStartInfo,
EnableRaisingEvents = true
};
var standardOutputResults = new TaskCompletionSource<string[]>();
process.OutputDataReceived += (sender, args) => {
if (args.Data != null)
standardOutput.Add(args.Data);
else
standardOutputResults.SetResult(standardOutput.ToArray());
};
var standardErrorResults = new TaskCompletionSource<string[]>();
process.ErrorDataReceived += (sender, args) => {
if (args.Data != null)
standardError.Add(args.Data);
else
standardErrorResults.SetResult(standardError.ToArray());
};
var processStartTime = new TaskCompletionSource<DateTime>();
process.Exited += async (sender, args) => {
// Since the Exited event can happen asynchronously to the output and error events,
// we await the task results for stdout/stderr to ensure they both closed. We must await
// the stdout/stderr tasks instead of just accessing the Result property due to behavior on MacOS.
// For more details, see the PR at https://github.com/jamesmanning/RunProcessAsTask/pull/16/
tcs.TrySetResult(
new ProcessResults(
process,
await processStartTime.Task.ConfigureAwait(false),
await standardOutputResults.Task.ConfigureAwait(false),
await standardErrorResults.Task.ConfigureAwait(false)
)
);
};
using (cancellationToken.Register(
() => {
tcs.TrySetCanceled();
try {
if (!process.HasExited)
process.Kill();
} catch (InvalidOperationException) { }
})) {
cancellationToken.ThrowIfCancellationRequested();
var startTime = DateTime.Now;
if (process.Start() == false)
{
tcs.TrySetException(new InvalidOperationException("Failed to start process"));
}
else
{
try
{
startTime = process.StartTime;
}
catch (Exception)
{
// best effort to try and get a more accurate start time, but if we fail to access StartTime
// (for instance, process has already existed), we still have a valid value to use.
}
processStartTime.SetResult(startTime);
process.BeginOutputReadLine();
process.BeginErrorReadLine();
}
return await tcs.Task.ConfigureAwait(false);
}
}
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace RunProcessAsTask
{
public sealed class ProcessResults : IDisposable
{
public ProcessResults(Process process, DateTime processStartTime, string[] standardOutput, string[] standardError)
{
Process = process;
ExitCode = process.ExitCode;
RunTime = process.ExitTime - processStartTime;
StandardOutput = standardOutput;
StandardError = standardError;
}
public Process Process { get; }
public int ExitCode { get; }
public TimeSpan RunTime { get; }
public string[] StandardOutput { get; }
public string[] StandardError { get; }
public void Dispose() { Process.Dispose(); }
}
}
// --------------------------------------------- Usage Examples -----------------------------------------
// Synchronous, just easier way of grabbing output / error / runtime for the process
Task<ProcessResults> processResults = ProcessEx.RunAsync("git.exe", "pull").Result;
Console.WriteLine("Exit code: " + processResults.ExitCode);
Console.WriteLine("Run time: " + processResults.RunTime);
Console.WriteLine("{0} lines of standard output", processResults.StandardOutput.Length);
foreach (var output in processResults.StandardOutput)
{
Console.WriteLine("Output line: " + output);
}
Console.WriteLine("{0} lines of standard error", processResults.StandardError.Length);
foreach (var error in processResults.StandardError)
{
Console.WriteLine("Error line: " + error);
}
// Provide timeout for running process
public async Task RunCommandWithTimeout(string filename, string arguments, TimeSpan timeout)
{
var processStartInfo = new ProcessStartInfo
{
FileName = filename,
Arguments = arguments,
};
try
{
using (var cancellationTokenSource = new CancellationTokenSource(timeout))
{
var processResults = await ProcessEx.RunAsync(processStartInfo, cancellationTokenSource.Token);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Timeout of {0} hit while trying to run {1} {2}", timeout, filename, arguments);
}
}
Run multiple commands with dependencies in an async fashion
public async Task ShowLastMatchingCommit(string regex)
{
var logProcessResults = await ProcessEx.RunAsync("git.exe", "log --pretty=oneline --all -n 1 -G" + regex);
if (logProcessResults.ExitCode != 0) return;
var stdoutSplit = logProcessResults.StandardOutput[0].Split(new[] { ' ' }, 2);
var commitHash = stdoutSplit[0];
var commitMessage = stdoutSplit[1];
Console.WriteLine("Last commit matching {0} was {1} and had commit message {2}", regex, commitHash, commitMessage);
var showProcessResults = await ProcessEx.RunAsync("git.exe", "show --pretty=fuller " + commitHash);
foreach (var stdoutLine in showProcessResults.StandardOutput)
{
Console.WriteLine("git show output: " + stdoutLine);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment