Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

namespace Aspire.Cli.Backchannel;

internal class AppHostIncompatibleException(string message, string requiredCapability) : Exception(message)
internal class AppHostIncompatibleException(string message, string requiredCapability, string? aspireHostingVersion = null) : Exception(message)
{
public string RequiredCapability { get; } = requiredCapability;
public string? AspireHostingVersion { get; } = aspireHostingVersion;
}
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/LogsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ private void OutputLogLine(ResourceLogLine logLine, OutputFormat format)
// Colorized output: assign a consistent color to each resource
var color = GetResourceColor(logLine.ResourceName);
var escapedContent = logLine.Content.EscapeMarkup();
AnsiConsole.MarkupLine($"[{color}][[{logLine.ResourceName}]][/] {escapedContent}");
AnsiConsole.MarkupLine($"[{color}][[{logLine.ResourceName.EscapeMarkup()}]][/] {escapedContent}");
}
else
{
Expand Down
16 changes: 8 additions & 8 deletions src/Aspire.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell

endpointsGrid.AddRow(
firstEndpoint ? new Align(new Markup($"[bold green]{endpointsLocalizedString}[/]:"), HorizontalAlignment.Right) : Text.Empty,
new Markup($"[bold]{resource}[/] [grey]has endpoint[/] [link={endpoint}]{endpoint}[/]")
new Markup($"[bold]{resource.EscapeMarkup()}[/] [grey]has endpoint[/] [link={endpoint.EscapeMarkup()}]{endpoint.EscapeMarkup()}[/]")
);

var endpointsPadder = new Padder(endpointsGrid, new Padding(3, 0));
Expand Down Expand Up @@ -348,31 +348,31 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
catch (AppHostIncompatibleException ex)
{
Telemetry.RecordError(ex.Message, ex);
return InteractionService.DisplayIncompatibleVersionError(ex, ex.RequiredCapability);
return InteractionService.DisplayIncompatibleVersionError(ex, ex.AspireHostingVersion ?? ex.RequiredCapability);
}
catch (CertificateServiceException ex)
{
var errorMessage = string.Format(CultureInfo.CurrentCulture, TemplatingStrings.CertificateTrustError, ex.Message.EscapeMarkup());
var errorMessage = string.Format(CultureInfo.CurrentCulture, TemplatingStrings.CertificateTrustError, ex.Message);
Telemetry.RecordError(errorMessage, ex);
InteractionService.DisplayError(errorMessage);
return ExitCodeConstants.FailedToTrustCertificates;
}
catch (FailedToConnectBackchannelConnection ex)
{
var errorMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ErrorConnectingToAppHost, ex.Message.EscapeMarkup());
var errorMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ErrorConnectingToAppHost, ex.Message);
Telemetry.RecordError(errorMessage, ex);
InteractionService.DisplayError(errorMessage);
// Don't display raw output - it's already in the log file
InteractionService.DisplayMessage("page_facing_up", string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath));
InteractionService.DisplayMessage("page_facing_up", string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath.EscapeMarkup()));
return ExitCodeConstants.FailedToDotnetRunAppHost;
}
catch (Exception ex)
{
var errorMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.UnexpectedErrorOccurred, ex.Message.EscapeMarkup());
var errorMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.UnexpectedErrorOccurred, ex.Message);
Telemetry.RecordError(errorMessage, ex);
InteractionService.DisplayError(errorMessage);
// Don't display raw output - it's already in the log file
InteractionService.DisplayMessage("page_facing_up", string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath));
InteractionService.DisplayMessage("page_facing_up", string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath.EscapeMarkup()));
return ExitCodeConstants.FailedToDotnetRunAppHost;
}
}
Expand Down Expand Up @@ -850,7 +850,7 @@ private async Task<int> ExecuteDetachedAsync(ParseResult parseResult, FileInfo?
_interactionService.DisplayMessage("magnifying_glass_tilted_right", string.Format(
CultureInfo.CurrentCulture,
RunCommandStrings.CheckLogsForDetails,
_fileLoggerProvider.LogFilePath));
_fileLoggerProvider.LogFilePath.EscapeMarkup()));

return ExitCodeConstants.FailedToDotnetRunAppHost;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/TelemetryLogsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,6 @@ private static void DisplayLogEntry(string resourceName, OtlpLogRecordJson log)
var severityColor = TelemetryCommandHelpers.GetSeverityColor(log.SeverityNumber);

var escapedBody = body.EscapeMarkup();
AnsiConsole.MarkupLine($"[grey]{timestamp}[/] [{severityColor}]{severity,-5}[/] [cyan]{resourceName}[/] {escapedBody}");
AnsiConsole.MarkupLine($"[grey]{timestamp}[/] [{severityColor}]{severity,-5}[/] [cyan]{resourceName.EscapeMarkup()}[/] {escapedBody}");
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/TelemetrySpansCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,6 @@ private static void DisplaySpanEntry(string resourceName, OtlpSpanJson span)
var durationStr = TelemetryCommandHelpers.FormatDuration(duration);

var escapedName = name.EscapeMarkup();
AnsiConsole.MarkupLine($"[grey]{shortSpanId}[/] [cyan]{resourceName,-15}[/] [{statusColor}]{statusText}[/] [white]{durationStr,8}[/] {escapedName}");
AnsiConsole.MarkupLine($"[grey]{shortSpanId}[/] [cyan]{resourceName.EscapeMarkup(),-15}[/] [{statusColor}]{statusText}[/] [white]{durationStr,8}[/] {escapedName}");
}
}
14 changes: 7 additions & 7 deletions src/Aspire.Cli/Interaction/ConsoleInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T

var prompt = new SelectionPrompt<T>()
.Title(promptText)
.UseConverter(choiceFormatter)
.UseConverter(item => choiceFormatter(item).EscapeMarkup())
.AddChoices(choices)
.PageSize(10)
.EnableSearch();
Expand Down Expand Up @@ -174,7 +174,7 @@ public async Task<IReadOnlyList<T>> PromptForSelectionsAsync<T>(string promptTex

var prompt = new MultiSelectionPrompt<T>()
.Title(promptText)
.UseConverter(choiceFormatter)
.UseConverter(item => choiceFormatter(item).EscapeMarkup())
.AddChoices(choices)
.PageSize(10);

Expand All @@ -189,9 +189,9 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri
DisplayError(InteractionServiceStrings.AppHostNotCompatibleConsiderUpgrading);
Console.WriteLine();
_outConsole.MarkupLine(
$"\t[bold]{InteractionServiceStrings.AspireHostingSDKVersion}[/]: {appHostHostingVersion}");
_outConsole.MarkupLine($"\t[bold]{InteractionServiceStrings.AspireCLIVersion}[/]: {cliInformationalVersion}");
_outConsole.MarkupLine($"\t[bold]{InteractionServiceStrings.RequiredCapability}[/]: {ex.RequiredCapability}");
$"\t[bold]{InteractionServiceStrings.AspireHostingSDKVersion}[/]: {appHostHostingVersion.EscapeMarkup()}");
_outConsole.MarkupLine($"\t[bold]{InteractionServiceStrings.AspireCLIVersion}[/]: {cliInformationalVersion.EscapeMarkup()}");
_outConsole.MarkupLine($"\t[bold]{InteractionServiceStrings.RequiredCapability}[/]: {ex.RequiredCapability.EscapeMarkup()}");
Console.WriteLine();
return ExitCodeConstants.AppHostIncompatible;
}
Expand Down Expand Up @@ -303,11 +303,11 @@ public void DisplayVersionUpdateNotification(string newerVersion, string? update
{
// Write to stderr to avoid corrupting stdout when JSON output is used
_errorConsole.WriteLine();
_errorConsole.MarkupLine(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.NewCliVersionAvailable, newerVersion));
_errorConsole.MarkupLine(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.NewCliVersionAvailable, newerVersion.EscapeMarkup()));

if (!string.IsNullOrEmpty(updateCommand))
{
_errorConsole.MarkupLine(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ToUpdateRunCommand, updateCommand));
_errorConsole.MarkupLine(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ToUpdateRunCommand, updateCommand.EscapeMarkup()));
}

_errorConsole.MarkupLine(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.MoreInfoNewCliVersion, UpdateUrl));
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Cli/Projects/DotNetAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ public async Task<int> PublishAsync(PublishContext context, CancellationToken ca
{
var exception = new AppHostIncompatibleException(
$"The app host is not compatible. Aspire.Hosting version: {compatibilityCheck.AspireHostingVersion}",
"Aspire.Hosting");
"Aspire.Hosting",
compatibilityCheck.AspireHostingVersion);
// Signal the backchannel completion source so the caller doesn't wait forever
context.BackchannelCompletionSource?.TrySetException(exception);
throw exception;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,48 +720,6 @@ public async Task PublishCommand_SingleInputPrompt_WhenStatusTextEqualsLabel_Sho
// Should show: [bold]Environment Name[/]
Assert.Equal("[bold]Environment Name[/]", promptCall.PromptText);
}

[Fact]
public async Task PublishCommand_SingleInputPrompt_EscapesSpectreMarkupInLabels()
{
// Arrange
using var workspace = TemporaryWorkspace.Create(outputHelper);
var promptBackchannel = new TestPromptBackchannel();
var consoleService = new TestConsoleInteractionServiceWithPromptTracking();

// Set up a single-input prompt with Spectre markup characters in both StatusText and Label
promptBackchannel.AddPrompt("markup-prompt", "Value [required]", InputTypes.Text, "Enter value [1-10]", isRequired: true);

// Set up the expected user response
consoleService.SetupStringPromptResponse("5");

var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
options.DotNetCliRunnerFactory = (sp) => CreateTestRunnerWithPromptBackchannel(promptBackchannel);
});

services.AddSingleton<IInteractionService>(consoleService);

var serviceProvider = services.BuildServiceProvider();
var command = serviceProvider.GetRequiredService<RootCommand>();

// Act
var result = command.Parse("publish");
var exitCode = await result.InvokeAsync().DefaultTimeout();

// Assert
Assert.Equal(0, exitCode);

// Verify that square brackets are properly escaped
var promptCalls = consoleService.StringPromptCalls;
Assert.Single(promptCalls);
var promptCall = promptCalls[0];

// Square brackets should be escaped to [[bracket]]
Assert.Contains("[[1-10]]", promptCall.PromptText);
Assert.Contains("[[required]]", promptCall.PromptText);
}
}

// Test implementation of IAppHostCliBackchannel that simulates prompt interactions
Expand Down
Loading
Loading