| File: Backchannel\ResourceSnapshotWatcher.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; namespace Aspire.Cli.Backchannel; /// <summary> /// Watches for resource snapshot changes from an AppHost backchannel connection /// and maintains an up-to-date collection of resources. /// </summary> internal sealed class ResourceSnapshotWatcher : IDisposable { private readonly IAppHostAuxiliaryBackchannel _connection; private readonly ConcurrentDictionary<string, ResourceSnapshot> _resources = new(StringComparers.ResourceName); private readonly CancellationTokenSource _cts = new(); private readonly TaskCompletionSource _initialLoadTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly Task _watchTask; private volatile Exception? _watchException; public ResourceSnapshotWatcher(IAppHostAuxiliaryBackchannel connection, bool includeHidden = false) { _connection = connection; IncludeHidden = includeHidden; _watchTask = WatchAsync(_cts.Token); } /// <summary> /// Gets a value indicating whether hidden resources are included by default in <see cref="GetResources()"/>. /// </summary> public bool IncludeHidden { get; } /// <summary> /// Waits until the initial resource snapshot load is complete. /// </summary> public Task WaitForInitialLoadAsync(CancellationToken cancellationToken = default) { return _initialLoadTcs.Task.WaitAsync(cancellationToken); } private async Task WatchAsync(CancellationToken cancellationToken) { try { var snapshots = await _connection.GetResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false); foreach (var snapshot in snapshots) { _resources[snapshot.Name] = snapshot; } _initialLoadTcs.TrySetResult(); await foreach (var snapshot in _connection.WatchResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false)) { _resources[snapshot.Name] = snapshot; } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { _initialLoadTcs.TrySetCanceled(cancellationToken); } catch (Exception ex) { if (!_initialLoadTcs.TrySetException(ex)) { // Initial load already completed; store for callers to detect. _watchException = ex; } } } /// <summary> /// Gets the exception that terminated the watch loop after the initial load, or <see langword="null"/> if the watch is still running. /// </summary> public Exception? WatchException => _watchException; private void EnsureInitialLoadComplete() { if (!_initialLoadTcs.Task.IsCompletedSuccessfully) { throw new InvalidOperationException("Initial resource snapshot load has not completed. Call WaitForInitialLoadAsync first."); } } /// <summary> /// Gets a resource snapshot by name, or <see langword="null"/> if not found. /// </summary> public ResourceSnapshot? GetResource(string name) { EnsureInitialLoadComplete(); return _resources.GetValueOrDefault(name); } /// <summary> /// Gets all current resource snapshots, using <see cref="IncludeHidden"/> to determine visibility. /// </summary> /// <returns>Resource snapshots, ordered by name.</returns> public IEnumerable<ResourceSnapshot> GetResources() { return GetResources(IncludeHidden); } /// <summary> /// Gets all current resource snapshots, including hidden resources. /// </summary> /// <returns>All resource snapshots, ordered by name.</returns> public IEnumerable<ResourceSnapshot> GetAllResources() { return GetResources(includeHidden: true); } private IEnumerable<ResourceSnapshot> GetResources(bool includeHidden) { EnsureInitialLoadComplete(); var snapshots = _resources.Values.AsEnumerable(); if (!includeHidden) { snapshots = snapshots.Where(s => !ResourceSnapshotMapper.IsHiddenResource(s)); } return snapshots.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase); } public void Dispose() { _cts.Cancel(); _cts.Dispose(); } }