From 268dae321524a9c33a0d226e5b93f891386fd308 Mon Sep 17 00:00:00 2001 From: Ella Hathaway Date: Mon, 18 May 2026 16:48:57 -0700 Subject: [PATCH] Route DisplayError output to stderr consistently DisplayError now always writes to _errorConsole (stderr) instead of following the MessageConsole property which defaults to stdout. This ensures error messages are visible when stdout is redirected. Extracted a shared WriteEmojiMessage helper to avoid code duplication between DisplayError and DisplayMessage. Fixes https://github.com/microsoft/aspire/issues/16136 --- .../Interaction/ConsoleInteractionService.cs | 14 +++-- .../ConsoleInteractionServiceTests.cs | 57 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs index 44dae5ce38e..36cfa874c21 100644 --- a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs @@ -369,17 +369,21 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri public void DisplayError(string errorMessage, bool allowMarkup = false) { var formatted = allowMarkup ? errorMessage : errorMessage.EscapeMarkup(); - DisplayMessage(KnownEmojis.CrossMark, $"[red bold]{formatted}[/]", allowMarkup: true); + // Always write errors to stderr so callers can capture them separately from stdout. + WriteEmojiMessage(_errorConsole, _stderrLogger, KnownEmojis.CrossMark, $"[red bold]{formatted}[/]", allowMarkup: true); } public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) + => WriteEmojiMessage(MessageConsole, MessageLogger, emoji, message, allowMarkup); + + private static void WriteEmojiMessage(IAnsiConsole target, ILogger logger, KnownEmoji emoji, string message, bool allowMarkup) { - if (MessageLogger.IsEnabled(LogLevel.Information)) + if (logger.IsEnabled(LogLevel.Information)) { // Only attempt to parse/remove markup when the message is expected to contain it. // Plain text messages may contain characters like '[' that would be rejected by the markup parser. var logMessage = allowMarkup ? message.RemoveMarkup() : message; - MessageLogger.LogInformation("{Message}", ConsoleHelpers.FormatEmojiPrefix(emoji, MessageConsole, replaceEmoji: true) + logMessage); + logger.LogInformation("{Message}", ConsoleHelpers.FormatEmojiPrefix(emoji, target, replaceEmoji: true) + logMessage); } var displayMessage = allowMarkup ? message : message.EscapeMarkup(); @@ -394,10 +398,10 @@ public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = grid.Columns[1].Padding = new Padding(0); grid.AddRow( - new Markup(ConsoleHelpers.FormatEmojiPrefix(emoji, MessageConsole)), + new Markup(ConsoleHelpers.FormatEmojiPrefix(emoji, target)), new Markup(displayMessage)); - MessageConsole.Write(grid); + target.Write(grid); } public void DisplayPlainText(string message) diff --git a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs index 047ebc4963f..fe9bb378159 100644 --- a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs @@ -610,6 +610,63 @@ public void DisplayError_WithMarkupCharactersInMessage_DoesNotDoubleEscape() Assert.DoesNotContain("[[Prod]]", outputString); } + [Fact] + public void DisplayError_WritesToStderr_RegardlessOfConsoleSetting() + { + var stdoutOutput = new StringBuilder(); + var stderrOutput = new StringBuilder(); + var stdoutConsole = AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = AnsiSupport.No, + ColorSystem = ColorSystemSupport.NoColors, + Out = new AnsiConsoleOutput(new StringWriter(stdoutOutput)) + }); + var stderrConsole = AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = AnsiSupport.No, + ColorSystem = ColorSystemSupport.NoColors, + Out = new AnsiConsoleOutput(new StringWriter(stderrOutput)) + }); + + var executionContext = CreateExecutionContext(); + var consoleEnvironment = new ConsoleEnvironment(stdoutConsole, stderrConsole); + var interactionService = new ConsoleInteractionService(consoleEnvironment, executionContext, TestHelpers.CreateInteractiveHostEnvironment(), NullLoggerFactory.Instance); + + // Console defaults to Standard (stdout), but errors should still go to stderr + interactionService.DisplayError("Something went wrong"); + + Assert.Empty(stdoutOutput.ToString()); + Assert.Contains("Something went wrong", stderrOutput.ToString()); + } + + [Fact] + public void DisplayMessage_WritesToStdout_WhenConsoleIsStandard() + { + var stdoutOutput = new StringBuilder(); + var stderrOutput = new StringBuilder(); + var stdoutConsole = AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = AnsiSupport.No, + ColorSystem = ColorSystemSupport.NoColors, + Out = new AnsiConsoleOutput(new StringWriter(stdoutOutput)) + }); + var stderrConsole = AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = AnsiSupport.No, + ColorSystem = ColorSystemSupport.NoColors, + Out = new AnsiConsoleOutput(new StringWriter(stderrOutput)) + }); + + var executionContext = CreateExecutionContext(); + var consoleEnvironment = new ConsoleEnvironment(stdoutConsole, stderrConsole); + var interactionService = new ConsoleInteractionService(consoleEnvironment, executionContext, TestHelpers.CreateInteractiveHostEnvironment(), NullLoggerFactory.Instance); + + interactionService.DisplayMessage(KnownEmojis.Information, "Status update"); + + Assert.Contains("Status update", stdoutOutput.ToString()); + Assert.Empty(stderrOutput.ToString()); + } + [Fact] public void DisplayMessage_WithUnescapedMarkup_AutoEscapesAndDoesNotThrow() {