From a8d2cdb2cdbf8e2fea3cf9ba3dfccb59a9f9cff6 Mon Sep 17 00:00:00 2001 From: Enis Necipoglu Date: Wed, 25 Mar 2026 13:07:13 +0300 Subject: [PATCH] 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 --- .../Volo/Abp/Cli/Utils/CmdHelper.cs | 16 +++---- .../Volo/Abp/Cli/Utils/CmdHelper_Tests.cs | 47 +++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Utils/CmdHelper_Tests.cs diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs index 5ccd9034cd..decf844131 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs +++ b/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(); diff --git a/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Utils/CmdHelper_Tests.cs b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Utils/CmdHelper_Tests.cs new file mode 100644 index 0000000000..1526a18d42 --- /dev/null +++ b/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(); + } + + [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-"); + } +}