Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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
14 changes: 7 additions & 7 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}]{endpoint.EscapeMarkup()}[/]")
);

var endpointsPadder = new Padder(endpointsGrid, new Padding(3, 0));
Expand Down Expand Up @@ -352,27 +352,27 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
}
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}");
}
}
12 changes: 6 additions & 6 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}");
$"\t[bold]{InteractionServiceStrings.AspireHostingSDKVersion}[/]: {appHostHostingVersion.EscapeMarkup()}");
_outConsole.MarkupLine($"\t[bold]{InteractionServiceStrings.AspireCLIVersion}[/]: {cliInformationalVersion}");
_outConsole.MarkupLine($"\t[bold]{InteractionServiceStrings.RequiredCapability}[/]: {ex.RequiredCapability}");
_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
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,50 @@ public async Task PublishCommand_SingleInputPrompt_EscapesSpectreMarkupInLabels(
Assert.Contains("[[1-10]]", promptCall.PromptText);
Assert.Contains("[[required]]", promptCall.PromptText);
}

[Fact]
public async Task PublishCommand_ChoicePrompt_WithSquareBracketsInOptions_DoesNotThrow()
{
// Arrange - simulates Azure subscription names containing brackets like "[Prod]"
// This is the root cause of https://github.com/dotnet/aspire/issues/13955
using var workspace = TemporaryWorkspace.Create(outputHelper);
var promptBackchannel = new TestPromptBackchannel();
var consoleService = new TestConsoleInteractionServiceWithPromptTracking();

var options = new List<KeyValuePair<string, string>>
{
new("sub-1", "DevDiv Test labs V2 [Prod] (00000000-0000-0000-0000-000000000001)"),
new("sub-2", "Azure SDK [Dev/Test] (00000000-0000-0000-0000-000000000002)"),
new("sub-3", "My <Team> Subscription (00000000-0000-0000-0000-000000000003)")
};
promptBackchannel.AddPrompt("subscription-prompt", "Azure Subscription", InputTypes.Choice, "Select subscription:", isRequired: true, options: options);

// Select the option with [Prod] in the name
consoleService.SetupSelectionResponse("DevDiv Test labs V2 [Prod] (00000000-0000-0000-0000-000000000001)");

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 the correct subscription was selected and sent back
Assert.Single(promptBackchannel.CompletedPrompts);
var completedPrompt = promptBackchannel.CompletedPrompts[0];
Assert.Equal("sub-1", completedPrompt.Answers[0].Value);
}
}

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