diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs index 9514676f717..a4705c162f3 100644 --- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs @@ -339,48 +339,76 @@ private async Task StartBackchannelAsync(IDotNetCliExecution? execution, string string[] cliArgs = [.. cliArgsList]; - var stdoutBuilder = new StringBuilder(); - var existingStandardOutputCallback = options.StandardOutputCallback; // Preserve the existing callback if it exists. - options.StandardOutputCallback = (line) => { - stdoutBuilder.AppendLine(line); - existingStandardOutputCallback?.Invoke(line); - }; + var existingStandardOutputCallback = options.StandardOutputCallback; + var existingStandardErrorCallback = options.StandardErrorCallback; - var stderrBuilder = new StringBuilder(); - var existingStandardErrorCallback = options.StandardErrorCallback; // Preserve the existing callback if it exists. - options.StandardErrorCallback = (line) => { - stderrBuilder.AppendLine(line); - existingStandardErrorCallback?.Invoke(line); - }; + // Retry when MSBuild returns success but produces no output, which can happen + // due to MSBuild server contention (e.g. when another AppHost build is running). + const int maxRetries = 3; + for (var attempt = 0; attempt < maxRetries; attempt++) + { + var stdoutBuilder = new StringBuilder(); + options.StandardOutputCallback = (line) => { + stdoutBuilder.AppendLine(line); + existingStandardOutputCallback?.Invoke(line); + }; - var exitCode = await ExecuteAsync( - args: cliArgs, - env: null, - projectFile: projectFile, - workingDirectory: projectFile.Directory!, - backchannelCompletionSource: null, - options: options, - cancellationToken: cancellationToken); + var stderrBuilder = new StringBuilder(); + options.StandardErrorCallback = (line) => { + stderrBuilder.AppendLine(line); + existingStandardErrorCallback?.Invoke(line); + }; - var stdout = stdoutBuilder.ToString(); - var stderr = stderrBuilder.ToString(); + var exitCode = await ExecuteAsync( + args: cliArgs, + env: null, + projectFile: projectFile, + workingDirectory: projectFile.Directory!, + backchannelCompletionSource: null, + options: options, + cancellationToken: cancellationToken); - if (exitCode != 0) - { - logger.LogError( - "Failed to get items and properties from project. Exit code was: {ExitCode}. See debug logs for more details. Stderr: {Stderr}, Stdout: {Stdout}", - exitCode, - stderr, - stdout - ); + var stdout = stdoutBuilder.ToString(); + var stderr = stderrBuilder.ToString(); - return (exitCode, null); - } - else - { - var json = JsonDocument.Parse(stdout!); + if (exitCode != 0) + { + logger.LogError( + "Failed to get items and properties from project. Exit code was: {ExitCode}. See debug logs for more details. Stderr: {Stderr}, Stdout: {Stdout}", + exitCode, + stderr, + stdout + ); + + return (exitCode, null); + } + + if (string.IsNullOrWhiteSpace(stdout)) + { + if (attempt < maxRetries - 1) + { + logger.LogWarning( + "dotnet msbuild returned exit code 0 but produced no output (attempt {Attempt}/{MaxRetries}). Retrying after delay. Stderr: {Stderr}", + attempt + 1, + maxRetries, + stderr); + await Task.Delay(TimeSpan.FromSeconds(attempt + 1), cancellationToken).ConfigureAwait(false); + continue; + } + + logger.LogWarning( + "dotnet msbuild returned exit code 0 but produced no output after {MaxRetries} attempts. Stderr: {Stderr}", + maxRetries, + stderr); + return (exitCode, null); + } + + var json = JsonDocument.Parse(stdout); return (exitCode, json); } + + // Should not be reached, but return failure as a safety net + return (1, null); } public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool noRestore, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) diff --git a/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs index b7b3ee942af..89b7f87a936 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs @@ -130,3 +130,4 @@ public async Task DetachFormatJsonProducesValidJson() await pendingRun; } } +