Browse Source

Fix stdout/stderr deadlock in CmdHelper.RunCmdAndGetOutput (#25155)

* Fix stdout/stderr deadlock in CmdHelper.RunCmdAndGetOutput

Read stdout and stderr concurrently to prevent pipe buffer deadlock.
The sequential ReadToEnd() calls caused a hang when child processes
(e.g. dotnet build) produced enough stderr output to fill the OS
pipe buffer (~4KB on Windows), since the parent blocked on stdout
while the child blocked on stderr.

Made-with: Cursor

* Simplify Task.WhenAll result handling per review

Made-with: Cursor

* Add test for CmdHelper to prevent stdout/stderr deadlock

* Reduce timeout in deadlock test for CmdHelper to 10 seconds

---------

Co-authored-by: maliming <malimings@gmail.com>
pull/25156/head
Enis Necipoglu 5 days ago
committed by GitHub
parent
commit
a8d2cdb2cd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs
  2. 47
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Utils/CmdHelper_Tests.cs

16
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs

@ -1,7 +1,8 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
@ -137,14 +138,11 @@ public class CmdHelper : ICmdHelper, ITransientDependency
process.Start();
using (var standardOutput = process.StandardOutput)
{
using (var standardError = process.StandardError)
{
output = standardOutput.ReadToEnd();
output += standardError.ReadToEnd();
}
}
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
var results = Task.WhenAll(outputTask, errorTask).GetAwaiter().GetResult();
output = results[0] + results[1];
process.WaitForExit();

47
framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Utils/CmdHelper_Tests.cs

@ -0,0 +1,47 @@
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Cli.Utils;
using Xunit;
namespace Volo.Abp.Cli.Utils;
public class CmdHelper_Tests : AbpCliTestBase
{
private readonly ICmdHelper _cmdHelper;
public CmdHelper_Tests()
{
_cmdHelper = GetRequiredService<ICmdHelper>();
}
[Fact]
public async Task RunCmdAndGetOutput_Should_Not_Deadlock_With_Large_Stdout_And_Stderr()
{
// Reproduces the deadlock bug where sequential ReadToEnd() on stdout then stderr
// would block indefinitely when both pipe buffers (~64 KB on Linux/macOS, ~4 KB on
// Windows) filled up simultaneously. The process was blocked writing to stderr while
// the caller was blocked waiting for stdout to close — a classic pipe deadlock.
//
// The fix reads both streams concurrently via ReadToEndAsync + Task.WhenAll, which
// drains both pipes at the same time and avoids the deadlock.
var command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? @"for /L %i in (1,1,3000) do @(echo stdout-line-%i & echo stderr-line-%i 1>&2)"
: "for i in $(seq 1 5000); do echo stdout-line-$i; echo stderr-line-$i >&2; done";
string output = null;
var cmdTask = Task.Run(() => output = _cmdHelper.RunCmdAndGetOutput(command));
var completed = await Task.WhenAny(cmdTask, Task.Delay(TimeSpan.FromSeconds(10)));
// The original sequential code deadlocked here; 10 s is a generous upper bound.
(completed == cmdTask).ShouldBeTrue(
"RunCmdAndGetOutput should not deadlock when both stdout and stderr produce large output");
await cmdTask;
output.ShouldNotBeNullOrWhiteSpace();
output.ShouldContain("stdout-line-");
output.ShouldContain("stderr-line-");
}
}
Loading…
Cancel
Save