├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml └── src ├── DummyConsoleApp ├── DummyConsoleApp.csproj └── Program.cs ├── RunProcessAsTask.Tests ├── ProcessExTests.cs └── RunProcessAsTask.Tests.csproj ├── RunProcessAsTask.sln └── RunProcessAsTask ├── ProcessEx.cs ├── ProcessEx.overloads.cs ├── ProcessResults.cs └── RunProcessAsTask.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Disable LF normalization for all files 2 | * -text 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Rr]elease/ 18 | 19 | # Visual Studio profiler 20 | *.psess 21 | *.vsp 22 | *.vspx 23 | 24 | # Guidance Automation Toolkit 25 | *.gpState 26 | 27 | # ReSharper is a .NET coding add-in 28 | _ReSharper* 29 | 30 | # NCrunch 31 | *.ncrunch* 32 | .*crunch*.local.xml 33 | 34 | # DocProject is a documentation generator add-in 35 | DocProject/buildhelp/ 36 | DocProject/Help/*.HxT 37 | DocProject/Help/*.HxC 38 | DocProject/Help/*.hhc 39 | DocProject/Help/*.hhk 40 | DocProject/Help/*.hhp 41 | DocProject/Help/Html2 42 | DocProject/Help/html 43 | 44 | # Click-Once directory 45 | publish 46 | 47 | # Publish Web Output 48 | *.Publish.xml 49 | 50 | # NuGet Packages Directory 51 | packages 52 | 53 | # Windows Azure Build Output 54 | csx 55 | *.build.csdef 56 | 57 | # Windows Store app package directory 58 | AppPackages/ 59 | 60 | # Others 61 | .vs/ 62 | 63 | src/Build/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: required 3 | mono: none 4 | dist: xenial 5 | dotnet: 2.2.300 6 | os: 7 | - osx 8 | - linux 9 | script: 10 | - dotnet build -c Release /m src 11 | - dotnet test -c Release --no-build src 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 James Manning 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RunProcessAsTask 2 | ================ 3 | 4 | [![Travis Build Status](https://travis-ci.org/jamesmanning/RunProcessAsTask.svg?branch=master)](https://travis-ci.org/jamesmanning/RunProcessAsTask) 5 | [![AppVeyor](https://img.shields.io/appveyor/ci/jamesmanning/RunProcessAsTask.svg)](https://ci.appveyor.com/project/jamesmanning/RunProcessAsTask) 6 | [![Coveralls](https://img.shields.io/coveralls/jamesmanning/RunProcessAsTask.svg)](https://coveralls.io/github/jamesmanning/RunProcessAsTask) 7 | 8 | [![GitHub issues](https://img.shields.io/github/issues/jamesmanning/RunProcessAsTask.svg)](https://github.com/jamesmanning/RunProcessAsTask/issues) 9 | [![GitHub stars](https://img.shields.io/github/stars/jamesmanning/RunProcessAsTask.svg)](https://github.com/jamesmanning/RunProcessAsTask/stargazers) 10 | [![GitHub forks](https://img.shields.io/github/forks/jamesmanning/RunProcessAsTask.svg)](https://github.com/jamesmanning/RunProcessAsTask/network) 11 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/jamesmanning/RunProcessAsTask/master/LICENSE) 12 | 13 | [![NuGet](https://img.shields.io/nuget/v/RunProcessAsTask.svg)](https://www.nuget.org/packages/RunProcessAsTask/) 14 | 15 | Simple wrapper around [System.Diagnostics.Process](http://msdn.microsoft.com/en-us/library/system.diagnostics.process.aspx) to expose it as a [System.Threading.Tasks.Task](http://msdn.microsoft.com/en-us/library/system.threading.tasks.task.aspx)<[ProcessResults](https://github.com/jamesmanning/RunProcessAsTask/blob/master/src/RunProcessAsTask/ProcessResults.cs)> 16 | 17 | Includes support for cancellation, timeout (via cancellation), and exposes the standard output, standard error, exit code, and run time of the process. 18 | 19 | To install into your project: 20 | 21 | ```powershell 22 | PM> Install-Package RunProcessAsTask 23 | ``` 24 | 25 | NOTE: if you need to handle stdout/stderr as they happen (while the process is still running), you may want to use Process directly or look at the [CliWrap](https://github.com/Tyrrrz/CliWrap) project 26 | 27 | # Example Usages 28 | 29 | ## Synchronous, just easier way of grabbing output / error / runtime for the process 30 | 31 | ```csharp 32 | Task processResults = ProcessEx.RunAsync("git.exe", "pull").Result; 33 | 34 | Console.WriteLine("Exit code: " + processResults.ExitCode); 35 | Console.WriteLine("Run time: " + processResults.RunTime); 36 | 37 | Console.WriteLine("{0} lines of standard output", processResults.StandardOutput.Length); 38 | foreach (var output in processResults.StandardOutput) 39 | { 40 | Console.WriteLine("Output line: " + output); 41 | } 42 | 43 | Console.WriteLine("{0} lines of standard error", processResults.StandardError.Length); 44 | foreach (var error in processResults.StandardError) 45 | { 46 | Console.WriteLine("Error line: " + error); 47 | } 48 | ``` 49 | 50 | ## Provide timeout for running process 51 | 52 | ```csharp 53 | public async Task RunCommandWithTimeout(string filename, string arguments, TimeSpan timeout) 54 | { 55 | var processStartInfo = new ProcessStartInfo 56 | { 57 | FileName = filename, 58 | Arguments = arguments, 59 | }; 60 | try 61 | { 62 | using (var cancellationTokenSource = new CancellationTokenSource(timeout)) 63 | { 64 | var processResults = await ProcessEx.RunAsync(processStartInfo, cancellationTokenSource.Token); 65 | } 66 | } 67 | catch (OperationCanceledException) 68 | { 69 | Console.WriteLine("Timeout of {0} hit while trying to run {1} {2}", timeout, filename, arguments); 70 | } 71 | } 72 | ``` 73 | 74 | ## Run multiple commands with dependencies in an async fashion 75 | 76 | ```csharp 77 | public async Task ShowLastMatchingCommit(string regex) 78 | { 79 | var logProcessResults = await ProcessEx.RunAsync("git.exe", "log --pretty=oneline --all -n 1 -G" + regex); 80 | if (logProcessResults.ExitCode != 0) return; 81 | 82 | var stdoutSplit = logProcessResults.StandardOutput[0].Split(new[] { ' ' }, 2); 83 | var commitHash = stdoutSplit[0]; 84 | var commitMessage = stdoutSplit[1]; 85 | Console.WriteLine("Last commit matching {0} was {1} and had commit message {2}", regex, commitHash, commitMessage); 86 | var showProcessResults = await ProcessEx.RunAsync("git.exe", "show --pretty=fuller " + commitHash); 87 | foreach (var stdoutLine in showProcessResults.StandardOutput) 88 | { 89 | Console.WriteLine("git show output: " + stdoutLine); 90 | } 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | image: Visual Studio 2017 3 | pull_requests: 4 | do_not_increment_build_number: true 5 | branches: 6 | only: 7 | - master 8 | skip_tags: true 9 | configuration: Release 10 | build_script: 11 | - cmd: dotnet build -c Release /m src 12 | test_script: 13 | - cmd: dotnet test -c Release --no-build src 14 | after_test: 15 | - dotnet test -c Release --no-build src/RunProcessAsTask.Tests -f netcoreapp2.2 /p:CollectCoverage=true 16 | - choco install codecov 17 | - codecov -h 18 | - codecov -f "src/RunProcessAsTask.Tests/coverage.opencover.xml" 19 | -------------------------------------------------------------------------------- /src/DummyConsoleApp/DummyConsoleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2; 4 | net472;$(TargetFrameworks) 5 | Exe 6 | ..\Build 7 | 8 | -------------------------------------------------------------------------------- /src/DummyConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | 6 | namespace DummyConsoleApp 7 | { 8 | public class Program 9 | { 10 | static void Main(string[] args) 11 | { 12 | int exitCodeToReturn = int.Parse(args[0]); 13 | int millisecondsToSleep = int.Parse(args[1]); 14 | int linesOfStandardOutput = int.Parse(args[2]); 15 | int linesOfStandardError = int.Parse(args[3]); 16 | 17 | Thread.Sleep(millisecondsToSleep); 18 | 19 | for (int i = 0; i < linesOfStandardOutput; i++) 20 | { 21 | Console.WriteLine("Standard output line #{0}", i + 1); 22 | } 23 | 24 | for (int i = 0; i < linesOfStandardError; i++) 25 | { 26 | Console.Error.WriteLine("Standard error line #{0}", i + 1); 27 | } 28 | 29 | Environment.Exit(exitCodeToReturn); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/RunProcessAsTask.Tests/ProcessExTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace RunProcessAsTask.Tests 10 | { 11 | public static class ProcessExTests 12 | { 13 | public class RunAsyncTests 14 | { 15 | readonly ITestOutputHelper output; 16 | public RunAsyncTests(ITestOutputHelper output) => this.output = output; 17 | 18 | [Fact] 19 | public void WhenProcessRunsNormally_ReturnsExpectedResults() 20 | { 21 | // Arrange 22 | const int expectedExitCode = 123; 23 | const int millisecondsToSleep = 5 * 1000; // set a minimum run time so we can validate it as part of the output 24 | const int expectedStandardOutputLineCount = 5; 25 | const int expectedStandardErrorLineCount = 3; 26 | 27 | var processStartInfo = DummyStartProcessArgs(expectedExitCode, millisecondsToSleep, expectedStandardOutputLineCount, expectedStandardErrorLineCount); 28 | 29 | // Act 30 | var task = ProcessEx.RunAsync(processStartInfo); 31 | 32 | // Assert 33 | Assert.NotNull(task); 34 | var results = task.Result; 35 | Assert.NotNull(results); 36 | Assert.Equal(TaskStatus.RanToCompletion, task.Status); 37 | Assert.NotNull(results.Process); 38 | Assert.True(results.Process.HasExited); 39 | Assert.NotNull(results.StandardOutput); 40 | Assert.NotNull(results.StandardError); 41 | 42 | var expectedStandardError = new[] { 43 | "Standard error line #1", 44 | "Standard error line #2", 45 | "Standard error line #3", 46 | }; 47 | Assert.Equal(expectedStandardError, results.StandardError); 48 | 49 | var expectedStandardOutput = new[] { 50 | "Standard output line #1", 51 | "Standard output line #2", 52 | "Standard output line #3", 53 | "Standard output line #4", 54 | "Standard output line #5", 55 | }; 56 | Assert.Equal(expectedStandardOutput, results.StandardOutput); 57 | 58 | Assert.Equal(expectedExitCode, results.ExitCode); 59 | Assert.Equal(expectedExitCode, results.Process.ExitCode); 60 | Assert.True(results.RunTime.TotalMilliseconds >= millisecondsToSleep); 61 | } 62 | 63 | [Fact] 64 | public void RunLotsOfOutputForPeriod() 65 | { 66 | // when this problem manifested with the older code, it would normally 67 | // trigger in this test within 5 to 10 seconds, so if it can run for 68 | // a full minute and not cause the output-truncation issue, we are probably fine 69 | var oneMinute = TimeSpan.FromMinutes(1); 70 | for (var stopwatch = Stopwatch.StartNew(); stopwatch.Elapsed < oneMinute;) 71 | Parallel.ForEach(Enumerable.Range(1, 100), index => WhenProcessReturnsLotsOfOutput_AllOutputCapturedCorrectly()); 72 | } 73 | 74 | readonly Random _random = new Random(); 75 | 76 | [Fact] 77 | public void WhenProcessReturnsLotsOfOutput_AllOutputCapturedCorrectly() 78 | { 79 | // Arrange 80 | const int expectedExitCode = 123; 81 | const int millisecondsToSleep = 0; // We want the process to exit right after printing the lines, so no wait time 82 | var expectedStandardOutputLineCount = _random.Next(1000, 100 * 1000); 83 | var expectedStandardErrorLineCount = _random.Next(1000, 100 * 1000); 84 | var processStartInfo = DummyStartProcessArgs(expectedExitCode, millisecondsToSleep, expectedStandardOutputLineCount, expectedStandardErrorLineCount); 85 | processStartInfo.CreateNoWindow = true; 86 | // Act 87 | var task = ProcessEx.RunAsync(processStartInfo); 88 | 89 | // Assert 90 | Assert.NotNull(task); 91 | var results = task.Result; 92 | Assert.NotNull(results); 93 | Assert.Equal(TaskStatus.RanToCompletion, task.Status); 94 | Assert.NotNull(results.Process); 95 | Assert.True(results.Process.HasExited); 96 | Assert.NotNull(results.StandardOutput); 97 | Assert.NotNull(results.StandardError); 98 | 99 | Assert.Equal(expectedExitCode, results.ExitCode); 100 | Assert.Equal(expectedExitCode, results.Process.ExitCode); 101 | Assert.True(results.RunTime.TotalMilliseconds >= millisecondsToSleep); 102 | Assert.Equal(expectedStandardOutputLineCount, results.StandardOutput.Length); 103 | Assert.Equal(expectedStandardErrorLineCount, results.StandardError.Length); 104 | 105 | var expectedStandardOutput = Enumerable.Range(1, expectedStandardOutputLineCount) 106 | .Select(x => "Standard output line #" + x) 107 | .ToArray(); 108 | var expectedStandardError = Enumerable.Range(1, expectedStandardErrorLineCount) 109 | .Select(x => "Standard error line #" + x) 110 | .ToArray(); 111 | Assert.Equal(expectedStandardOutput, results.StandardOutput); 112 | Assert.Equal(expectedStandardError, results.StandardError); 113 | } 114 | 115 | static ProcessStartInfo DummyStartProcessArgs( 116 | int expectedExitCode, 117 | int millisecondsToSleep, 118 | int expectedStandardOutputLineCount, 119 | int expectedStandardErrorLineCount) 120 | => new ProcessStartInfo( 121 | "dotnet", 122 | string.Join(" ", "../netcoreapp2.2/DummyConsoleApp.dll", expectedExitCode, millisecondsToSleep, expectedStandardOutputLineCount, expectedStandardErrorLineCount) 123 | ) 124 | { 125 | WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory, 126 | }; 127 | 128 | [Fact] 129 | public void WhenProcessTimesOut_TaskIsCanceled() 130 | { 131 | // Arrange 132 | const int expectedExitCode = 123; 133 | const int millisecondsForTimeout = 3 * 1000; 134 | const int millisecondsToSleep = 5 * 1000; 135 | const int expectedStandardOutputLineCount = 5; 136 | const int expectedStandardErrorLineCount = 3; 137 | 138 | // Act 139 | 140 | var processStartInfo = DummyStartProcessArgs(expectedExitCode, millisecondsToSleep, expectedStandardOutputLineCount, expectedStandardErrorLineCount); 141 | var cancellationToken = new CancellationTokenSource(millisecondsForTimeout).Token; 142 | var task = ProcessEx.RunAsync(processStartInfo, cancellationToken); 143 | Assert.NotNull(task); 144 | 145 | // Assert 146 | var aggregateException = Assert.Throws(() => task.Result); 147 | Assert.Equal(1, aggregateException.InnerExceptions.Count); 148 | var innerException = aggregateException.InnerExceptions.Single(); 149 | var canceledException = Assert.IsType(innerException); 150 | Assert.NotNull(canceledException); 151 | Assert.True(cancellationToken.IsCancellationRequested); 152 | Assert.Equal(TaskStatus.Canceled, task.Status); 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/RunProcessAsTask.Tests/RunProcessAsTask.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2; 4 | net472;$(TargetFrameworks) 5 | Library 6 | ..\Build 7 | 8 | 9 | 10 | false 11 | opencover 12 | [xunit.*]*,[DummyConsoleApp]* 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/RunProcessAsTask.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunProcessAsTask", "RunProcessAsTask\RunProcessAsTask.csproj", "{0FC71DC7-B864-4FE4-A77A-AC833044E749}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DummyConsoleApp", "DummyConsoleApp\DummyConsoleApp.csproj", "{899350EA-14B2-4600-BF14-72319CD73007}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunProcessAsTask.Tests", "RunProcessAsTask.Tests\RunProcessAsTask.Tests.csproj", "{30F3FD69-434C-43E8-9965-5F6CFFA348AD}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{35081C7D-4C5D-48BB-B0CF-866719EC3788}" 13 | ProjectSection(SolutionItems) = preProject 14 | ..\.gitattributes = ..\.gitattributes 15 | ..\.gitignore = ..\.gitignore 16 | ..\.travis.yml = ..\.travis.yml 17 | ..\appveyor.yml = ..\appveyor.yml 18 | ..\LICENSE = ..\LICENSE 19 | ..\README.md = ..\README.md 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {0FC71DC7-B864-4FE4-A77A-AC833044E749}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {0FC71DC7-B864-4FE4-A77A-AC833044E749}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {0FC71DC7-B864-4FE4-A77A-AC833044E749}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {0FC71DC7-B864-4FE4-A77A-AC833044E749}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {899350EA-14B2-4600-BF14-72319CD73007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {899350EA-14B2-4600-BF14-72319CD73007}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {899350EA-14B2-4600-BF14-72319CD73007}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {899350EA-14B2-4600-BF14-72319CD73007}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {30F3FD69-434C-43E8-9965-5F6CFFA348AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {30F3FD69-434C-43E8-9965-5F6CFFA348AD}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {30F3FD69-434C-43E8-9965-5F6CFFA348AD}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {30F3FD69-434C-43E8-9965-5F6CFFA348AD}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /src/RunProcessAsTask/ProcessEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace RunProcessAsTask 8 | { 9 | public static partial class ProcessEx 10 | { 11 | /// 12 | /// Runs asynchronous process. 13 | /// 14 | /// The that contains the information that is used to start the process, including the file name and any command-line arguments. 15 | /// List that lines written to standard output by the process will be added to 16 | /// List that lines written to standard error by the process will be added to 17 | /// The token to monitor for cancellation requests. 18 | public static async Task RunAsync(ProcessStartInfo processStartInfo, List standardOutput, List standardError, CancellationToken cancellationToken) 19 | { 20 | // force some settings in the start info so we can capture the output 21 | processStartInfo.UseShellExecute = false; 22 | processStartInfo.RedirectStandardOutput = true; 23 | processStartInfo.RedirectStandardError = true; 24 | 25 | var tcs = new TaskCompletionSource(); 26 | 27 | var process = new Process { 28 | StartInfo = processStartInfo, 29 | EnableRaisingEvents = true 30 | }; 31 | 32 | var standardOutputResults = new TaskCompletionSource(); 33 | process.OutputDataReceived += (sender, args) => { 34 | if (args.Data != null) 35 | standardOutput.Add(args.Data); 36 | else 37 | standardOutputResults.SetResult(standardOutput.ToArray()); 38 | }; 39 | 40 | var standardErrorResults = new TaskCompletionSource(); 41 | process.ErrorDataReceived += (sender, args) => { 42 | if (args.Data != null) 43 | standardError.Add(args.Data); 44 | else 45 | standardErrorResults.SetResult(standardError.ToArray()); 46 | }; 47 | 48 | var processStartTime = new TaskCompletionSource(); 49 | 50 | process.Exited += async (sender, args) => { 51 | // Since the Exited event can happen asynchronously to the output and error events, 52 | // we await the task results for stdout/stderr to ensure they both closed. We must await 53 | // the stdout/stderr tasks instead of just accessing the Result property due to behavior on MacOS. 54 | // For more details, see the PR at https://github.com/jamesmanning/RunProcessAsTask/pull/16/ 55 | tcs.TrySetResult( 56 | new ProcessResults( 57 | process, 58 | await processStartTime.Task.ConfigureAwait(false), 59 | await standardOutputResults.Task.ConfigureAwait(false), 60 | await standardErrorResults.Task.ConfigureAwait(false) 61 | ) 62 | ); 63 | }; 64 | 65 | using (cancellationToken.Register( 66 | () => { 67 | tcs.TrySetCanceled(); 68 | try { 69 | if (!process.HasExited) 70 | process.Kill(); 71 | } catch (InvalidOperationException) { } 72 | })) { 73 | cancellationToken.ThrowIfCancellationRequested(); 74 | 75 | var startTime = DateTime.Now; 76 | if (process.Start() == false) 77 | { 78 | tcs.TrySetException(new InvalidOperationException("Failed to start process")); 79 | } 80 | else 81 | { 82 | try 83 | { 84 | startTime = process.StartTime; 85 | } 86 | catch (Exception) 87 | { 88 | // best effort to try and get a more accurate start time, but if we fail to access StartTime 89 | // (for instance, process has already existed), we still have a valid value to use. 90 | } 91 | processStartTime.SetResult(startTime); 92 | 93 | process.BeginOutputReadLine(); 94 | process.BeginErrorReadLine(); 95 | } 96 | 97 | return await tcs.Task.ConfigureAwait(false); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/RunProcessAsTask/ProcessEx.overloads.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace RunProcessAsTask 7 | { 8 | // these overloads match the ones in Process.Start to make it a simpler transition for callers 9 | // see http://msdn.microsoft.com/en-us/library/system.diagnostics.process.start.aspx 10 | public partial class ProcessEx 11 | { 12 | /// 13 | /// Runs asynchronous process. 14 | /// 15 | /// An application or document which starts the process. 16 | public static Task RunAsync(string fileName) 17 | => RunAsync(new ProcessStartInfo(fileName)); 18 | 19 | /// 20 | /// Runs asynchronous process. 21 | /// 22 | /// An application or document which starts the process. 23 | /// The token to monitor for cancellation requests. 24 | public static Task RunAsync(string fileName, CancellationToken cancellationToken) 25 | => RunAsync(new ProcessStartInfo(fileName), cancellationToken); 26 | 27 | /// 28 | /// Runs asynchronous process. 29 | /// 30 | /// An application or document which starts the process. 31 | /// Command-line arguments to pass to the application when the process starts. 32 | public static Task RunAsync(string fileName, string arguments) 33 | => RunAsync(new ProcessStartInfo(fileName, arguments)); 34 | 35 | /// 36 | /// Runs asynchronous process. 37 | /// 38 | /// An application or document which starts the process. 39 | /// Command-line arguments to pass to the application when the process starts. 40 | /// The token to monitor for cancellation requests. 41 | public static Task RunAsync(string fileName, string arguments, CancellationToken cancellationToken) 42 | => RunAsync(new ProcessStartInfo(fileName, arguments), cancellationToken); 43 | 44 | /// 45 | /// Runs asynchronous process. 46 | /// 47 | /// The that contains the information that is used to start the process, including the file name and any command-line arguments. 48 | public static Task RunAsync(ProcessStartInfo processStartInfo) 49 | => RunAsync(processStartInfo, CancellationToken.None); 50 | 51 | /// 52 | /// Runs asynchronous process. 53 | /// 54 | /// The that contains the information that is used to start the process, including the file name and any command-line arguments. 55 | /// The token to monitor for cancellation requests. 56 | public static Task RunAsync(ProcessStartInfo processStartInfo, CancellationToken cancellationToken) 57 | => RunAsync(processStartInfo, new List(), new List(), cancellationToken); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/RunProcessAsTask/ProcessResults.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace RunProcessAsTask 5 | { 6 | /// 7 | /// Contains information about process after it has exited. 8 | /// 9 | public sealed class ProcessResults : IDisposable 10 | { 11 | public ProcessResults(Process process, DateTime processStartTime, string[] standardOutput, string[] standardError) 12 | { 13 | Process = process; 14 | ExitCode = process.ExitCode; 15 | RunTime = process.ExitTime - processStartTime; 16 | StandardOutput = standardOutput; 17 | StandardError = standardError; 18 | } 19 | 20 | public Process Process { get; } 21 | public int ExitCode { get; } 22 | public TimeSpan RunTime { get; } 23 | public string[] StandardOutput { get; } 24 | public string[] StandardError { get; } 25 | public void Dispose() { Process.Dispose(); } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/RunProcessAsTask/RunProcessAsTask.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Library 4 | netstandard1.4;netstandard2.0; 5 | net45;$(TargetFrameworks) 6 | James Manning, Max Cheetham and Eamon Nerbonne 7 | 8 | Simple wrapper around System.Diagnostics.Process to expose it as a System.Threading.Tasks.Task<ProcessResults> 9 | 10 | Includes support for cancellation, timeout (via cancellation), and exposes the standard output, standard error, exit code, and run time of the process. 11 | 12 | 1.2.4 13 | false 14 | http://opensource.org/licenses/MIT 15 | https://github.com/jamesmanning/RunProcessAsTask/ 16 | Add support for passing in external lists for stdout/stderr collecting, fix corner case for processes that complete too quickly 17 | 18 | 19 | 20 | 21 | --------------------------------------------------------------------------------