Skip to content

Instantly share code, notes, and snippets.

@kmcginnes
Last active August 29, 2015 14:21
Show Gist options
  • Save kmcginnes/fcaefc865335fc68f1e7 to your computer and use it in GitHub Desktop.
Save kmcginnes/fcaefc865335fc68f1e7 to your computer and use it in GitHub Desktop.
var results = await new ProcessBuilder("some_command.exe")
.WithTimeout(TimeSpan.FromMinutes(5))
.WithArguments("/blah foo")
.WithPath(@"C:\bin")
.ExecuteAsync();
if(results.ExitCode == 0)
{
Console.WriteLine("Output:");
results.StandardOutput.ForEach(line => Console.WriteLine(line));
}
else
{
Console.WriteLine("Exited with error code {0} and output:", results.ExitCode);
results.StandardError.ForEach(line => Console.WriteLine(line));
}
public interface IProcessResults
{
int ExitCode { get; set; }
IEnumerable<string> StandardOutput { get; set; }
IEnumerable<string> StandardError { get; set; }
}
public class ProcessBuilder
{
PathString _path;
string _arguments;
readonly CancellationTokenSource _cancellationTokenSource;
// Provided for tests only
public static Func<Task<IProcessResults>> ExecuteOverride = null;
public ProcessBuilder(string executableName)
{
if(string.IsNullOrWhiteSpace(executableName))
throw new ArgumentNullException("executableName", @"Must provide the name of the executable to execute. This can be either just the executable (e.g. 'cmd.exe') or the full file path (e.g. 'C:\Windows\System32\cmd.exe').");
_path = new PathString(executableName);
_cancellationTokenSource = new CancellationTokenSource();
_arguments = string.Empty;
}
public ProcessBuilder WithPath(PathString path)
{
_path = path.Combine(_path.Parts.Last());
return this;
}
public ProcessBuilder WithTimeout(TimeSpan timeout)
{
_cancellationTokenSource.CancelAfter(timeout);
return this;
}
public ProcessBuilder WithArguments(string arguments)
{
_arguments = arguments;
return this;
}
public Task<IProcessResults> ExecuteAsync()
{
// Allow tests to override the execution of processes
if (ExecuteOverride != null)
{
return ExecuteOverride();
}
var cancellationToken = _cancellationTokenSource.Token;
var tcs = new TaskCompletionSource<IProcessResults>();
var standardOutput = new List<string>();
var standardError = new List<string>();
var process = new Process
{
EnableRaisingEvents = true,
StartInfo = new ProcessStartInfo(_path, _arguments)
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
},
};
process.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
standardOutput.Add(args.Data);
}
};
process.ErrorDataReceived += (sender, args) =>
{
if (args.Data != null)
{
standardError.Add(args.Data);
}
};
process.Exited += (sender, args) =>
tcs.TrySetResult(
new ProcessResults
{
ExitCode = process.ExitCode,
StandardOutput = standardOutput.AsEnumerable(),
StandardError = standardError.AsEnumerable(),
});
cancellationToken.Register(() =>
{
tcs.TrySetCanceled();
process.CloseMainWindow();
});
cancellationToken.ThrowIfCancellationRequested();
if (process.Start() == false)
{
tcs.TrySetException(new InvalidOperationException(
String.Format("Failed to start process '{0}'", _path)));
}
return tcs.Task;
}
}
public class ProcessResults : IProcessResults
{
public ProcessResults()
{
ExitCode = 0;
StandardOutput = Enumerable.Empty<string>();
StandardError = Enumerable.Empty<string>();
}
public int ExitCode { get; set; }
public IEnumerable<string> StandardOutput { get; set; }
public IEnumerable<string> StandardError { get; set; }
}
@wgv-zbonham
Copy link

Looks nice! I'm a big fan of that 'builder' pattern and this use of gist! :)

We have several areas of code where we will spawn a process and I'm not sure there is any test coverage there precisely because of the problem your solving. It would be interesting to see actual test implementation in those areas.

Can you give me an example of when you imagine using the Timeout behavior? For example, we've got some processes that can run a long time (FFMPEG) and we generally want them to run to completion (success or error). Just trying to think of a use case where I wanted it terminate if it ran beyond some time span? I'm betting you've got an example use case or it wouldn't be in there. :)

On that same note, whats the behavior of CloseMainWindow on a non-gui process? Almost think at that point we'd want to just Kill() the process?

I had to add the following 2 lines after .Start() to receive any output:

process.BeginOutputReadLine();
process.BeginErrorReadLine();

Curious about the dependency on PathString and whatever IEnumerable.ForEach extension method being used?

The only other feedback is that Process implements IDisposable so shouldn't it be in a using block?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment