Process.WaitForExit() asynchronously
process.EnableRaisingEvents = true;
process.Exited += [EventHandler]
process.WaitForExit(int32) asynchronously
You'll need to alter the method signature and the apply the result from the process itself. For example consider the following:
/// <summary>
/// Waits asynchronously for the process to exit.
/// </summary>
/// <param name="process">The process to wait for cancellation.</param>
/// <param name="cancellationToken">A cancellation token. If invoked, the task will return
/// immediately as canceled.</param>
/// <returns>A Task representing waiting for the process to end.</returns>
public static Task WaitForExitAsync(this Process process,
int milliseconds,
CancellationToken cancellationToken = default(CancellationToken))
{
if (process.HasExited)
{
return Task.CompletedTask;
}
var tcs = new TaskCompletionSource<object>();
process.EnableRaisingEvents = true;
process.Exited += (sender, args) => tcs.TrySetResult(null);
if (cancellationToken != default(CancellationToken))
{
cancellationToken.Register(tcs.SetCanceled);
}
return process.HasExited
? Task.CompletedTask
: Task.WhenAny(tcs.Task, Task.Delay(milliseconds));
}
Now, if the process has not ended the task will still be returned after the delay. An alternative would be to do a Task.Run
with the invocation to the desired overload like this:
/// <summary>
/// Waits asynchronously for the process to exit.
/// </summary>
/// <param name="process">The process to wait for cancellation.</param>
/// <param name="cancellationToken">A cancellation token. If invoked, the task will return
/// immediately as canceled.</param>
/// <returns>A Task representing waiting for the process to end.</returns>
public static Task WaitForExitAsync(this Process process,
int milliseconds,
CancellationToken cancellationToken = default(CancellationToken)) =>
process.HasExited
? Task.CompletedTask
: Task.Run(() => process.WaitForExit(milliseconds), cancellationToken);
}
Process WaitForExit and get return value async
You can use this code:
void Login(string pathtofile)
{
Process process = new Process();
process.StartInfo.FileName = pathtofile;
process.EnableRaisingEvents = true;
process.Exited += new EventHandler(process_Exited);
process.Start();
}
void process_Exited(object sender, EventArgs e)
{
Process p = (Process)sender;
int exitCode = p.ExitCode;
}
But note that the Login function will directly exit after starting the process so you cannot return an integer value. You get the exit code in the function process_exited
Async process start and wait for it to finish
The .NET 5 introduced the new API Process.WaitForExitAsync
, that allows to wait asynchronously for the completion of a process. It offers the same functionality with the existing Process.WaitForExit
, with the only difference being that the waiting is asynchronous, so it does not block the calling thread.
Usage example:
private async void button1_Click(object sender, EventArgs e)
{
string filePath = Path.Combine
(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
Guid.NewGuid().ToString() + ".txt"
);
File.WriteAllText(filePath, "Hello World!");
try
{
using Process process = new();
process.StartInfo.FileName = "Notepad.exe";
process.StartInfo.Arguments = filePath;
process.Start();
await process.WaitForExitAsync();
}
finally
{
File.Delete(filePath);
}
MessageBox.Show("Done!");
}
In the above example the UI remains responsive while the user interacts with the opened file. The UI thread would be blocked if the WaitForExit
had been used instead.
WaitForExitAsync with a timeout
You need to use CancellationTokenSource
. It has a ctor which accepts a TimeSpan
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
await CMD.WaitForExitAsync(timeoutSignal.Token);
} catch (OperationCanceledException)
{
CMD.Kill();
}
When the CTS signals then the awaited operation will throw an OperationCanceledException
. So you need to wrap your await
call into a try
-catch
to handle cancelled operation properly.
UPDATE #1: Capture STDOUT with async wait of exit
Naive approach
First let me share with you the naive version of the code
Console.WriteLine("Launch ping with fifteen retries");
var terminal = Process.Start(new ProcessStartInfo("/sbin/ping")
{
RedirectStandardOutput = true,
Arguments = "-c 15 stackoverflow.com",
UseShellExecute = false,
});
_ = Task.Run(() =>
{
string line = null;
while ((line = terminal.StandardOutput.ReadLine()) != null)
Console.WriteLine(line);
});
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
await terminal.WaitForExitAsync(timeoutSignal.Token);
Console.WriteLine("Ping has been Finished");
}
catch (OperationCanceledException)
{
terminal.Kill();
Console.WriteLine("Ping has been Terminated");
}
- I'm using .NET on a Macintosh machine so, I don't have
ping.exe
rather than I can run/sbin/ping
command - I ping stackoverflow fifteen times to make sure the command runs more than 3 seconds
- I've moved the
StandardOutput
reading to a separate thread (Task.Run
)- Without that, the cancellation signal will not have any effect
- The rest of the code same as above + debug logging
Suggested approach
The Process
class does expose a capability to read data asynchronously from the StandardOutput
without the need to do extra tricks
Console.WriteLine("Launch ping with fifteen retries");
var terminal = new Process()
{
StartInfo = new ProcessStartInfo("/sbin/ping")
{
RedirectStandardOutput = true,
Arguments = "-c 15 stackoverflow.com",
UseShellExecute = false,
}
};
terminal.OutputDataReceived += (s, e) => Console.WriteLine(e.Data);
terminal.Start();
terminal.BeginOutputReadLine();
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
await terminal.WaitForExitAsync(timeoutSignal.Token);
Console.WriteLine("Ping has been Finished");
}
catch (OperationCanceledException)
{
terminal.Kill();
Console.WriteLine("Ping has been Terminated");
}
Let me highlight only the differences
- Rather than starting the process right away, first we create a Process and specify its
StartInfo
property - Then we subscribe to the
OutputDataReceived
event- Its EventArgs'
Data
property contains the newly available information
- Its EventArgs'
- After the subscription we can call the
Start
method - And finally we need to call the
BeginOutputReadLine
method to tell the Process fire the above event handler whenever new data is available on the standard output
ProcessStartInfo hanging on WaitForExit ? Why?
The problem is that if you redirect StandardOutput
and/or StandardError
the internal buffer can become full. Whatever order you use, there can be a problem:
- If you wait for the process to exit before reading
StandardOutput
the process can block trying to write to it, so the process never ends. - If you read from
StandardOutput
using ReadToEnd then your process can block if the process never closesStandardOutput
(for example if it never terminates, or if it is blocked writing toStandardError
).
The solution is to use asynchronous reads to ensure that the buffer doesn't get full. To avoid any deadlocks and collect up all output from both StandardOutput
and StandardError
you can do this:
EDIT: See answers below for how avoid an ObjectDisposedException if the timeout occurs.
using (Process process = new Process())
{
process.StartInfo.FileName = filename;
process.StartInfo.Arguments = arguments;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
process.OutputDataReceived += (sender, e) => {
if (e.Data == null)
{
outputWaitHandle.Set();
}
else
{
output.AppendLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data == null)
{
errorWaitHandle.Set();
}
else
{
error.AppendLine(e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (process.WaitForExit(timeout) &&
outputWaitHandle.WaitOne(timeout) &&
errorWaitHandle.WaitOne(timeout))
{
// Process completed. Check process.ExitCode here.
}
else
{
// Timed out.
}
}
}
Is there any async equivalent of Process.Start?
Process.Start()
only starts the process, it doesn't wait until it finishes, so it doesn't make much sense to make it async
. If you still want to do it, you can do something like await Task.Run(() => Process.Start(fileName))
.
But, if you want to asynchronously wait for the process to finish, you can use the Exited
event together with TaskCompletionSource
:
static Task<int> RunProcessAsync(string fileName)
{
var tcs = new TaskCompletionSource<int>();
var process = new Process
{
StartInfo = { FileName = fileName },
EnableRaisingEvents = true
};
process.Exited += (sender, args) =>
{
tcs.SetResult(process.ExitCode);
process.Dispose();
};
process.Start();
return tcs.Task;
}
Using System.Diagnostics.Process asynchronously, how should I ensure that I've received the last output before determining it has exited?
There is an interlock when you explicitly use Process.WaitForExit(-1). It won't return until the asynchronous readers for stdout and stderr have indicated end-of-file status. Call it in your Exited event handler. You must use a timeout of -1 or this won't work. Or just WaitForExit(). Which is fine, you know it already exited.
Related Topics
An Implementation of the Fast Fourier Transform (Fft) in C#
No Output to Console from a Wpf Application
Programmatically Determine a Duration of a Locked Workstation
How to Detect the Character Encoding of a Text File
What Is Cool About Generics, Why Use Them
How to Add a Blend Behavior in a Style Setter
Reading PDF Content with Itextsharp Dll in Vb.Net or C#
How to Get Memory Available or Used in C#
How to Start Winform App Minimized to Tray
Is There Any Async Equivalent of Process.Start
Binding List<T> to Datagridview in Winform
Validate Image from File in C#
Check for Column Name in a SQLdatareader Object
Generating Permutations of a Set (Most Efficiently)