Created
January 4, 2021 15:00
-
-
Save elusive/d70d30d8217e871645e749fb419180d8 to your computer and use it in GitHub Desktop.
Extension methods and result class for executing at the command line from C#, with a timeout.
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
public static class ProcessStartInfoExtensions | |
{ | |
public const int WaitForExitMilliseconds = 500; | |
/// <summary> | |
/// Starts a process with support for cancellation via the | |
/// provided token. Console output for the process is redirected | |
/// to the provided delegates. | |
/// </summary> | |
/// <param name="processStartInfo"><see cref="ProcessStartInfo" /> instance.</param> | |
/// <param name="processExitedCallback">Handler for process exit.</param> | |
/// <param name="token">Cancel token.</param> | |
/// <param name="timeoutMs">Milliseconds used to timeout waiting for exit.</param> | |
/// <returns>Exit code result wrapped in a task.</returns> | |
public static async Task<int> StartWithTimeoutAsync( | |
[NotNull] this ProcessStartInfo processStartInfo, | |
Action<TerminalResult> processExitedCallback, | |
CancellationToken token, | |
int timeoutMs = 3500) | |
{ | |
processStartInfo.RedirectStandardError = true; | |
processStartInfo.RedirectStandardOutput = true; | |
processStartInfo.UseShellExecute = false; | |
processStartInfo.CreateNoWindow = true; | |
var standardOut = new List<string>(); | |
var standardErr = new List<string>(); | |
var tcs = new TaskCompletionSource<int>(); | |
var ps = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true }; | |
ps.Exited += (obj, args) => | |
{ | |
ps.WaitForExit(); | |
var exitCode = ps.ExitCode; | |
var result = new TerminalResult( | |
exitCode == 0 ? TerminalResultEnum.Success : TerminalResultEnum.Failure, | |
exitCode, | |
standardOut, | |
standardErr); | |
processExitedCallback?.Invoke(result); | |
ps.CancelErrorRead(); | |
ps.CancelOutputRead(); | |
ps.Dispose(); | |
tcs.TrySetResult(exitCode); | |
}; | |
// register cancel handling | |
await using (token.Register(() => | |
{ | |
tcs.TrySetCanceled(); | |
try | |
{ | |
if (ps.HasExited) return; | |
ps.Kill(); | |
ps.WaitForExit(WaitForExitMilliseconds); | |
} | |
finally | |
{ | |
ps.Dispose(); | |
} | |
})) | |
{ | |
// in case canceled before starting | |
if (token.IsCancellationRequested) throw new OperationCanceledException(token); | |
// bind output and error handling | |
ps.OutputDataReceived += (sender, args) => | |
{ | |
if (!string.IsNullOrEmpty(args.Data)) standardOut.Add(args.Data); | |
}; | |
ps.ErrorDataReceived += (sender, args) => | |
{ | |
if (!string.IsNullOrEmpty(args.Data)) standardErr.Add(args.Data); | |
}; | |
// start process in a thread that we can join | |
ps.Start(); | |
ps.BeginErrorReadLine(); | |
ps.BeginOutputReadLine(); | |
ps.WaitForExit(timeoutMs); | |
// check to see if still running after timeout | |
if (!ps.HasExited) | |
{ | |
ps.Kill(true); | |
} | |
return await tcs.Task.ConfigureAwait(false); | |
} | |
} | |
} |
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
public sealed class TerminalResult | |
{ | |
private const string ToStringFormat = @"Terminal process [{0}] result was: {1}"; | |
public TerminalResult( | |
TerminalResultEnum result, | |
int exitCode, | |
IEnumerable<string> standardOut, | |
IEnumerable<string> standardErr) | |
{ | |
Result = result; | |
ExitCode = exitCode; | |
StandardError = standardErr; | |
StandardOutput = standardOut; | |
} | |
public IEnumerable<string> StandardOutput { get; } | |
public IEnumerable<string> StandardError { get; } | |
public TerminalResultEnum Result { get; } | |
public int ExitCode { get; } | |
public override string ToString() | |
{ | |
return string.Format(ToStringFormat, "Terminal Process", Result); | |
} | |
} |
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
public enum TerminalResultEnum | |
{ | |
Unknown, | |
Success, | |
Failure | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is an ever improving work in progress. Finally decided to make a gist with this version. I have used this multiple times. This time adding in the timeout so that commands that remain open at the command line can be run and cancelled after the initial output is collected. For example browsing with dns-sd remains open.