Process.Waitforexit() Asynchronously

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
  • 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 closes StandardOutput (for example if it never terminates, or if it is blocked writing to StandardError).

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



Leave a reply



Submit