Last active
August 29, 2015 14:21
-
-
Save kmcginnes/fcaefc865335fc68f1e7 to your computer and use it in GitHub Desktop.
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
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)); | |
} |
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 interface IProcessResults | |
{ | |
int ExitCode { get; set; } | |
IEnumerable<string> StandardOutput { get; set; } | |
IEnumerable<string> StandardError { get; set; } | |
} |
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 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; | |
} | |
} |
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 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; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 justKill()
the process?I had to add the following 2 lines after
.Start()
to receive any output:Curious about the dependency on
PathString
and whateverIEnumerable.ForEach
extension method being used?The only other feedback is that
Process
implementsIDisposable
so shouldn't it be in a using block?