Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide default parameters in project resources #7333

Closed
Closed
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
115 changes: 106 additions & 9 deletions src/Aspire.Dashboard/Model/ResourceSourceViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;

namespace Aspire.Dashboard.Model;

public class ResourceSourceViewModel(string value, string? contentAfterValue, string valueToVisualize, string tooltip)
Expand All @@ -12,31 +14,35 @@ public class ResourceSourceViewModel(string value, string? contentAfterValue, st

internal static ResourceSourceViewModel? GetSourceViewModel(ResourceViewModel resource)
{
var executablePath = resource.TryGetExecutablePath(out var path) ? path : null;

(string? ArgumentsString, string FullCommandLine)? commandLineInfo = null;
string? executablePath;
(string? NonDefaultArguments, string FullCommandLine)? commandLineInfo;

if (resource.TryGetExecutableArguments(out var arguments))
if (resource.TryGetExecutablePath(out var path))
{
executablePath = path;
commandLineInfo = GetCommandLineInfo(resource, executablePath);
}
else
{
var argumentsString = arguments.IsDefaultOrEmpty ? null : string.Join(" ", arguments);
commandLineInfo = (ArgumentsString: argumentsString, $"{executablePath} {argumentsString}");
executablePath = null;
commandLineInfo = null;
}

// NOTE projects are also executables, so we have to check for projects first
if (resource.IsProject() && resource.TryGetProjectPath(out var projectPath))
{
if (commandLineInfo is { ArgumentsString: { } argumentsString, FullCommandLine: { } fullCommandLine })
if (commandLineInfo is { NonDefaultArguments: { } argumentsString, FullCommandLine: { } fullCommandLine })
{
return new ResourceSourceViewModel(value: Path.GetFileName(projectPath), contentAfterValue: argumentsString, valueToVisualize: fullCommandLine, tooltip: fullCommandLine);
}

// default to project path if there is no executable path or executable arguments
return new ResourceSourceViewModel(value: Path.GetFileName(projectPath), contentAfterValue: commandLineInfo?.ArgumentsString, valueToVisualize: projectPath, tooltip: projectPath);
return new ResourceSourceViewModel(value: Path.GetFileName(projectPath), contentAfterValue: commandLineInfo?.NonDefaultArguments, valueToVisualize: projectPath, tooltip: projectPath);
}

if (executablePath is not null)
{
return new ResourceSourceViewModel(value: Path.GetFileName(executablePath), contentAfterValue: commandLineInfo?.ArgumentsString, valueToVisualize: commandLineInfo?.FullCommandLine ?? executablePath, tooltip: commandLineInfo?.FullCommandLine ?? string.Empty);
return new ResourceSourceViewModel(value: Path.GetFileName(executablePath), contentAfterValue: commandLineInfo?.NonDefaultArguments, valueToVisualize: commandLineInfo?.FullCommandLine ?? executablePath, tooltip: commandLineInfo?.FullCommandLine ?? string.Empty);
}

if (resource.TryGetContainerImage(out var containerImage))
Expand All @@ -51,4 +57,95 @@ public class ResourceSourceViewModel(string value, string? contentAfterValue, st

return null;
}

/**
* Returns information about command line arguments, stripping out DCP default arguments, if any exist.
* The defaults come from DcpExecutor#PrepareProjectExecutables and need to be kept in sync
*/
private static (string? NonDefaultArguments, string FullCommandLine)? GetCommandLineInfo(ResourceViewModel resource, string executablePath)
{
if (resource.TryGetExecutableArguments(out var arguments))
{
if (arguments.IsDefaultOrEmpty)
{
return (NonDefaultArguments: null, FullCommandLine: executablePath);
}

var escapedArguments = arguments.Select(EscapeCommandLineArgument).ToList();

if (resource.IsProject())
{
if (escapedArguments.Count > 3 && escapedArguments.Take(3).SequenceEqual(["run", "--no-build", "--project"], StringComparers.CommandLineArguments))
{
escapedArguments.RemoveRange(0, 4); // remove the project path too
}
else if (escapedArguments.Count > 4 && escapedArguments.Take(4).SequenceEqual(["watch", "--non-interactive", "--no-hot-reload", "--project"], StringComparers.CommandLineArguments))
{
escapedArguments.RemoveRange(0, 5); // remove the project path too
}

if (escapedArguments.Count > 1 && string.Equals(escapedArguments[0], "-c", StringComparisons.CommandLineArguments))
{
escapedArguments.RemoveRange(0, 2);
}

if (escapedArguments.Count > 0 && string.Equals(escapedArguments[0], "--no-launch-profile", StringComparisons.CommandLineArguments))
{
escapedArguments.RemoveAt(0);
}
}

var cleanedArguments = escapedArguments.Count == 0 ? null : string.Join(' ', escapedArguments);
var fullCommandLine = resource.TryGetProjectPath(out var projectPath)
? AppendArgumentsIfNotEmpty(projectPath, cleanedArguments)
: AppendArgumentsIfNotEmpty(executablePath, cleanedArguments);

return (NonDefaultArguments: cleanedArguments ?? string.Empty, FullCommandLine: fullCommandLine);

static string AppendArgumentsIfNotEmpty(string s, string? arguments) => arguments is null ? s : $"{s} {arguments}";
}

return null;

// This method doesn't account for all cases, but does the most common
static string EscapeCommandLineArgument(string argument)
{
if (string.IsNullOrEmpty(argument))
{
return "\"\"";
}

if (argument.Contains(' ') || argument.Contains('"') || argument.Contains('\\'))
{
var escapedArgument = new StringBuilder();
escapedArgument.Append('"');

for (int i = 0; i < argument.Length; i++)
{
char c = argument[i];
switch (c)
{
case '\\':
// Escape backslashes
escapedArgument.Append('\\');
escapedArgument.Append('\\');
break;
case '"':
// Escape quotes
escapedArgument.Append('\\');
escapedArgument.Append('"');
break;
default:
escapedArgument.Append(c);
break;
}
}

escapedArgument.Append('"');
return escapedArgument.ToString();
}

return argument;
}
}
}
2 changes: 2 additions & 0 deletions src/Shared/StringComparers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal static class StringComparers
public static StringComparer OtlpSpanId => StringComparer.Ordinal;
public static StringComparer HealthReportPropertyValue => StringComparer.Ordinal;
public static StringComparer CultureName => StringComparer.OrdinalIgnoreCase;
public static StringComparer CommandLineArguments => StringComparer.Ordinal;
}

internal static class StringComparisons
Expand All @@ -49,4 +50,5 @@ internal static class StringComparisons
public static StringComparison OtlpSpanId => StringComparison.Ordinal;
public static StringComparison HealthReportPropertyValue => StringComparison.Ordinal;
public static StringComparison CultureName => StringComparison.OrdinalIgnoreCase;
public static StringComparison CommandLineArguments => StringComparison.Ordinal;
}
20 changes: 17 additions & 3 deletions tests/Aspire.Dashboard.Tests/Model/ResourceSourceViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,29 @@ void AddStringProperty(string propertyName, string? propertyValue)
data.Add(new TestData(
ResourceType: "Project",
ExecutablePath: "path/to/executable",
ExecutableArguments: ["arg1", "arg2"],
ExecutableArguments: ["run", "--no-build", "--project", "path/to/project", "--no-launch-profile", "arg1", "arg2"],
ProjectPath: "path/to/project",
ContainerImage: null,
SourceProperty: null),
new ResourceSourceViewModel(
value: "project",
contentAfterValue: "arg1 arg2",
valueToVisualize: "path/to/executable arg1 arg2",
tooltip: "path/to/executable arg1 arg2"));
valueToVisualize: "path/to/project arg1 arg2",
tooltip: "path/to/project arg1 arg2"));

// Project with executable arguments (dotnet watch)
data.Add(new TestData(
ResourceType: "Project",
ExecutablePath: "path/to/executable",
ExecutableArguments: ["watch", "--non-interactive", "--no-hot-reload", "--project", "path/to/project", "-c", "CONFIG", "--no-launch-profile", "arg1", "arg2"],
ProjectPath: "path/to/project",
ContainerImage: null,
SourceProperty: null),
new ResourceSourceViewModel(
value: "project",
contentAfterValue: "arg1 arg2",
valueToVisualize: "path/to/project arg1 arg2",
tooltip: "path/to/project arg1 arg2"));

// Project without executable arguments
data.Add(new TestData(
Expand Down