WaitForExitAsync with a timeout

Viewed 641

So I noticed that there's a method called WaitForExit that accepts and int as argument (milliseconds) so if the process isn't able to exit itself, I just killed it after some seconds.

Something like this.

if (!CMD.WaitForExit(3000))
    CMD.Kill();

The thing is that meanwhile I'd like to save the output, so I noticed that there's an async method WaitForExitAsync but this one doesn't accept these milliseconds.

// Wait for exit async...
// Meanwhile save the output till it kills itself.

while (CMD.StandardOutput.ReadLine() != null) 
    standard_output = StandardOutput.ReadLine();

Any idea how to do this? Thanks!

3 Answers

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

The new .NET 6 comes with WaitAsync method that allows asynchronously waiting for a task with a timeout.

await process.WaitForExitAsync().WaitAsync(TimeSpan.FromMilliseconds(1000));

if (!process.HasExited)
{
    //timeout!
}

It have a argument takes millisecond

WaitForExitAsync(process, timeout);
Related