Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ internal sealed class AsyncStreamReader : IDisposable
{
private const int DefaultBufferSize = 1024; // Byte buffer size

/// <summary>
/// Default timeout in milliseconds to wait for redirected streams to complete after the process exits.
/// </summary>
private const int StreamDrainDefaultTimeoutMs = 300;

private readonly Stream _stream;
private readonly Decoder _decoder;
private readonly byte[] _byteBuffer;
Expand Down Expand Up @@ -253,7 +258,29 @@ private bool FlushMessageQueue(bool rethrowInNewThread)
}
}

internal Task EOF => _readToBufferTask ?? Task.CompletedTask;
internal void CancelDueToProcessExit()
{
Task? task = _readToBufferTask;
if (task is not null && !task.Wait(StreamDrainDefaultTimeoutMs))
{
_cts.Cancel();
task.GetAwaiter().GetResult();
}
}

internal async Task CancelDueToProcessExitAsync(CancellationToken cancellationToken)
{
Task? task = _readToBufferTask;
if (task is not null)
{
Task completed = await Task.WhenAny(task, Task.Delay(StreamDrainDefaultTimeoutMs, cancellationToken)).ConfigureAwait(false);
if (completed != task)
{
_cts.Cancel();
await task.ConfigureAwait(false);
}
}
}

public void Dispose()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,10 @@ private bool WaitForExitCore(int milliseconds)
bool exited = GetWaitState().WaitForExit(milliseconds);
Debug.Assert(exited || milliseconds != Timeout.Infinite);

if (exited && milliseconds == Timeout.Infinite) // if we have a hard timeout, we cannot wait for the streams
if (exited)
{
_output?.EOF.GetAwaiter().GetResult();
_error?.EOF.GetAwaiter().GetResult();
_output?.CancelDueToProcessExit();
_error?.CancelDueToProcessExit();
}

return exited;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,10 @@ private bool WaitForExitCore(int milliseconds)
}
finally
{
// If we have a hard timeout, we cannot wait for the streams
if (milliseconds == Timeout.Infinite)
if (_signaled)
{
_output?.EOF.GetAwaiter().GetResult();
_error?.EOF.GetAwaiter().GetResult();
_output?.CancelDueToProcessExit();
_error?.CancelDueToProcessExit();
}

handle?.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1527,7 +1527,7 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken = default
// exception up to the user
if (HasExited)
{
await WaitUntilOutputEOF(cancellationToken).ConfigureAwait(false);
await CancelStreamReadersDueToProcessExitAsync(cancellationToken).ConfigureAwait(false);
return;
}

Expand All @@ -1554,24 +1554,24 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken = default
}
}

// Wait until output streams have been drained
await WaitUntilOutputEOF(cancellationToken).ConfigureAwait(false);
// Cancel stream readers after process exit with a default timeout
await CancelStreamReadersDueToProcessExitAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
Exited -= handler;
}

async Task WaitUntilOutputEOF(CancellationToken cancellationToken)
async Task CancelStreamReadersDueToProcessExitAsync(CancellationToken cancellationToken)
{
if (_output is not null)
{
await _output.EOF.WaitAsync(cancellationToken).ConfigureAwait(false);
await _output.CancelDueToProcessExitAsync(cancellationToken).ConfigureAwait(false);
}

if (_error is not null)
{
await _error.EOF.WaitAsync(cancellationToken).ConfigureAwait(false);
await _error.CancelDueToProcessExitAsync(cancellationToken).ConfigureAwait(false);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -671,5 +671,61 @@ public async Task WaitForExitAsync_NotDirected_ThrowsInvalidOperationException()
var process = new Process();
await Assert.ThrowsAsync<InvalidOperationException>(() => process.WaitForExitAsync());
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData("WaitForExit")]
[InlineData("WaitForExitInt")]
[InlineData("WaitForExitTimeSpan")]
[InlineData("WaitForExitAsync")]
public async Task WaitForExit_WithGrandChildProcess_DoesNotHang(string waitMethod)
{
Process child = CreateProcess(() =>
{
using Process grandChild = CreateProcess(SleepForEightHours);
grandChild.Start();
Console.WriteLine(grandChild.Id);

return RemoteExecutor.SuccessExitCode;
});
child.StartInfo.RedirectStandardOutput = true;
child.Start();

string grandChildPidStr = await child.StandardOutput.ReadLineAsync();
int grandChildPid = int.Parse(grandChildPidStr);

var stopwatch = Stopwatch.StartNew();

switch (waitMethod)
{
case "WaitForExit":
child.WaitForExit();
break;
case "WaitForExitInt":
Assert.True(child.WaitForExit(WaitInMS));
break;
case "WaitForExitTimeSpan":
Assert.True(child.WaitForExit(TimeSpan.FromMilliseconds(WaitInMS)));
break;
case "WaitForExitAsync":
await child.WaitForExitAsync();
break;
}

stopwatch.Stop();
Assert.True(stopwatch.Elapsed.TotalSeconds < 2, $"WaitForExit took {stopwatch.Elapsed.TotalSeconds:F1}s, expected < 2s");

try
{
Process.GetProcessById(grandChildPid).Kill();
}
catch (Exception) { }
}

private static int SleepForEightHours()
{
Thread.Sleep(TimeSpan.FromHours(8));

return RemoteExecutor.SuccessExitCode;
}
}
}
Loading