Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
84 changes: 84 additions & 0 deletions src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,36 @@ public async Task<List<ResourceSnapshot>> GetResourceSnapshotsAsync(Cancellation
}
}

/// <inheritdoc />
public async Task<List<ResourceSnapshot>> GetResourceSnapshotsAsync(bool includeHiddenResources, CancellationToken cancellationToken = default)
{
if (!includeHiddenResources)
{
return await GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false);
}

var rpc = EnsureConnected();

_logger?.LogDebug("Getting resource snapshots including hidden resources");

try
{
var snapshots = await rpc.InvokeWithCancellationAsync<List<ResourceSnapshot>>(
"GetResourceSnapshotsAsync",
[includeHiddenResources],
cancellationToken).ConfigureAwait(false) ?? [];

snapshots.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));

return snapshots;
}
catch (RemoteMethodNotFoundException ex)
{
_logger?.LogDebug(ex, "GetResourceSnapshotsAsync(bool) RPC method not available on the remote AppHost. Falling back to visible resources only.");
return await GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false);
}
}

/// <inheritdoc />
public async IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -313,6 +343,60 @@ public async IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync([Enu
}
}

/// <inheritdoc />
public async IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync(bool includeHiddenResources, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (!includeHiddenResources)
{
await foreach (var snapshot in WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false))
{
yield return snapshot;
}

yield break;
}

var rpc = EnsureConnected();

_logger?.LogDebug("Starting resource snapshots watch including hidden resources");

IAsyncEnumerable<ResourceSnapshot>? snapshots;
var fallbackToVisibleResources = false;
try
{
snapshots = await rpc.InvokeWithCancellationAsync<IAsyncEnumerable<ResourceSnapshot>>(
"WatchResourceSnapshotsAsync",
[includeHiddenResources],
cancellationToken).ConfigureAwait(false);
}
catch (RemoteMethodNotFoundException ex)
{
_logger?.LogDebug(ex, "WatchResourceSnapshotsAsync(bool) RPC method not available on the remote AppHost. Falling back to visible resources only.");
snapshots = null;
fallbackToVisibleResources = true;
}

if (fallbackToVisibleResources)
{
await foreach (var snapshot in WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false))
{
yield return snapshot;
}

yield break;
}

if (snapshots is null)
{
yield break;
}

await foreach (var snapshot in snapshots.WithCancellation(cancellationToken).ConfigureAwait(false))
{
yield return snapshot;
}
}

/// <inheritdoc />
public async IAsyncEnumerable<ResourceLogLine> GetResourceLogsAsync(
string? resourceName = null,
Expand Down
16 changes: 16 additions & 0 deletions src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,29 @@ internal interface IAppHostAuxiliaryBackchannel : IDisposable
/// <returns>The dashboard URL state including health and resolved dashboard URLs.</returns>
Task<DashboardUrlsState?> GetDashboardUrlsAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gets the current resource snapshots from the AppHost.
/// </summary>
/// <param name="includeHiddenResources">Whether hidden resources should be included.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list of resource snapshots representing current state.</returns>
Task<List<ResourceSnapshot>> GetResourceSnapshotsAsync(bool includeHiddenResources, CancellationToken cancellationToken = default);

/// <summary>
/// Gets the current resource snapshots from the AppHost.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list of resource snapshots representing current state.</returns>
Task<List<ResourceSnapshot>> GetResourceSnapshotsAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Watches for resource snapshot changes and streams them from the AppHost.
/// </summary>
/// <param name="includeHiddenResources">Whether hidden resources should be included.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of resource snapshots as they change.</returns>
IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync(bool includeHiddenResources, CancellationToken cancellationToken = default);

/// <summary>
/// Watches for resource snapshot changes and streams them from the AppHost.
/// </summary>
Expand Down
24 changes: 19 additions & 5 deletions src/Aspire.Cli/Commands/DescribeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ internal sealed class DescribeCommand : BaseCommand
{
Description = DescribeCommandStrings.JsonOptionDescription
};
private static readonly Option<bool> s_includeHiddenOption = new("--include-hidden")
{
Description = DescribeCommandStrings.IncludeHiddenOptionDescription
};

public DescribeCommand(
IInteractionService interactionService,
Expand All @@ -109,6 +113,7 @@ public DescribeCommand(
Options.Add(s_appHostOption);
Options.Add(s_followOption);
Options.Add(s_formatOption);
Options.Add(s_includeHiddenOption);
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
Expand All @@ -119,6 +124,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
var passedAppHostProjectFile = parseResult.GetValue(s_appHostOption);
var follow = parseResult.GetValue(s_followOption);
var format = parseResult.GetValue(s_formatOption);
var includeHidden = parseResult.GetValue(s_includeHiddenOption);

var result = await _connectionResolver.ResolveConnectionAsync(
passedAppHostProjectFile,
Expand All @@ -139,20 +145,20 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
// Get dashboard URL and resource snapshots in parallel before
// dispatching to the snapshot or watch path.
var dashboardUrlsTask = connection.GetDashboardUrlsAsync(cancellationToken);
var snapshotsTask = connection.GetResourceSnapshotsAsync(cancellationToken);
var snapshotsTask = connection.GetResourceSnapshotsAsync(includeHidden, cancellationToken);

await Task.WhenAll(dashboardUrlsTask, snapshotsTask).ConfigureAwait(false);

var dashboardBaseUrl = TelemetryCommandHelpers.ExtractDashboardBaseUrl((await dashboardUrlsTask.ConfigureAwait(false))?.BaseUrlWithLoginToken);
var snapshots = await snapshotsTask.ConfigureAwait(false);
var snapshots = FilterHiddenResources(await snapshotsTask.ConfigureAwait(false), includeHidden);

// Pre-resolve colors for all resource names so that assignment is
// deterministic regardless of which resources are displayed.
_resourceColorMap.ResolveAll(snapshots.Select(s => ResourceSnapshotMapper.GetResourceName(s, snapshots)));

if (follow)
{
return await ExecuteWatchAsync(connection, snapshots, dashboardBaseUrl, resourceName, format, cancellationToken);
return await ExecuteWatchAsync(connection, snapshots, dashboardBaseUrl, resourceName, format, includeHidden, cancellationToken);
}
else
{
Expand Down Expand Up @@ -192,7 +198,7 @@ private int ExecuteSnapshot(IReadOnlyList<ResourceSnapshot> snapshots, string? d
return ExitCodeConstants.Success;
}

private async Task<int> ExecuteWatchAsync(IAppHostAuxiliaryBackchannel connection, IReadOnlyList<ResourceSnapshot> initialSnapshots, string? dashboardBaseUrl, string? resourceName, OutputFormat format, CancellationToken cancellationToken)
private async Task<int> ExecuteWatchAsync(IAppHostAuxiliaryBackchannel connection, IReadOnlyList<ResourceSnapshot> initialSnapshots, string? dashboardBaseUrl, string? resourceName, OutputFormat format, bool includeHidden, CancellationToken cancellationToken)
{
// Maintain a dictionary of the current state per resource for relationship resolution
// and display name deduplication. Keyed by snapshot.Name so each resource has exactly
Expand All @@ -208,8 +214,13 @@ private async Task<int> ExecuteWatchAsync(IAppHostAuxiliaryBackchannel connectio
var lastDisplayedContent = new Dictionary<string, object>(StringComparers.ResourceName);

// Stream resource snapshots
await foreach (var snapshot in connection.WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false))
await foreach (var snapshot in connection.WatchResourceSnapshotsAsync(includeHidden, cancellationToken).ConfigureAwait(false))
{
if (!includeHidden && snapshot.IsHidden)
{
continue;
}

// Update the dictionary with the latest state for this resource
allResources[snapshot.Name] = snapshot;

Expand Down Expand Up @@ -296,6 +307,9 @@ private void DisplayResourcesTable(IReadOnlyList<ResourceSnapshot> snapshots)
_interactionService.DisplayRenderable(table);
}

private static IReadOnlyList<ResourceSnapshot> FilterHiddenResources(IReadOnlyList<ResourceSnapshot> snapshots, bool includeHidden)
=> includeHidden ? snapshots : snapshots.Where(s => !s.IsHidden).ToList();

private static ResourceDisplayState BuildResourceDisplayState(ResourceSnapshot snapshot, IReadOnlyList<ResourceSnapshot> allResources)
{
var displayName = ResourceSnapshotMapper.GetResourceName(snapshot, allResources);
Expand Down
6 changes: 6 additions & 0 deletions src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Aspire.Cli/Resources/DescribeCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@
<data name="JsonOptionDescription" xml:space="preserve">
<value>Output format (Table or Json)</value>
</data>
<data name="IncludeHiddenOptionDescription" xml:space="preserve">
<value>Include hidden resources in the output</value>
</data>
<data name="NoAppHostFound" xml:space="preserve">
<value>No AppHost project found.</value>
</data>
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.tr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading