| File: CancellationTokenRegistry.cs | Web Access |
| Project: src\src\Aspire.Hosting.RemoteHost\Aspire.Hosting.RemoteHost.csproj (Aspire.Hosting.RemoteHost) |
// 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.Hosting.RemoteHost; /// <summary> /// Manages CancellationTokenSource instances for cross-process cancellation. /// When a callback with a CancellationToken parameter is invoked, the host creates /// a CancellationTokenSource and passes a token ID to the guest. The guest can /// then cancel the token by calling the cancelToken RPC method. /// </summary> internal sealed class CancellationTokenRegistry : IDisposable { private readonly ConcurrentDictionary<string, CancellationTokenSource> _sources = new(); private int _counter; private bool _disposed; /// <summary> /// Creates a new CancellationTokenSource and returns its ID and token. /// The ID can be passed to the guest, which can use it to cancel the token. /// </summary> /// <returns>A tuple of (tokenId, cancellationToken).</returns> public (string TokenId, CancellationToken Token) Create() { ObjectDisposedException.ThrowIf(_disposed, this); var id = $"ct_{Interlocked.Increment(ref _counter)}"; var cts = new CancellationTokenSource(); if (!_sources.TryAdd(id, cts)) { cts.Dispose(); throw new InvalidOperationException($"Failed to register cancellation token with ID '{id}'"); } return (id, cts.Token); } /// <summary> /// Creates a new CancellationTokenSource linked to an existing token. /// This is useful when you need to combine multiple cancellation sources. /// </summary> /// <param name="linkedToken">The token to link to.</param> /// <returns>A tuple of (tokenId, cancellationToken).</returns> public (string TokenId, CancellationToken Token) CreateLinked(CancellationToken linkedToken) { ObjectDisposedException.ThrowIf(_disposed, this); var id = $"ct_{Interlocked.Increment(ref _counter)}"; var cts = CancellationTokenSource.CreateLinkedTokenSource(linkedToken); if (!_sources.TryAdd(id, cts)) { cts.Dispose(); throw new InvalidOperationException($"Failed to register cancellation token with ID '{id}'"); } return (id, cts.Token); } /// <summary> /// Cancels a CancellationTokenSource by its ID. /// </summary> /// <param name="tokenId">The token ID returned from Create().</param> /// <returns>True if the token was found and cancelled, false if not found.</returns> public bool Cancel(string tokenId) { if (_sources.TryGetValue(tokenId, out var cts)) { try { cts.Cancel(); return true; } catch (ObjectDisposedException) { // Already disposed, that's fine return false; } } return false; } /// <summary> /// Unregisters and disposes a CancellationTokenSource by its ID. /// Call this when the callback completes to clean up resources. /// </summary> /// <param name="tokenId">The token ID to unregister.</param> /// <returns>True if the token was found and unregistered, false if not found.</returns> public bool Unregister(string tokenId) { if (_sources.TryRemove(tokenId, out var cts)) { cts.Dispose(); return true; } return false; } /// <summary> /// Gets the CancellationToken for a given ID. /// </summary> /// <param name="tokenId">The token ID.</param> /// <param name="token">The cancellation token if found.</param> /// <returns>True if found, false otherwise.</returns> public bool TryGetToken(string tokenId, out CancellationToken token) { if (_sources.TryGetValue(tokenId, out var cts)) { token = cts.Token; return true; } token = default; return false; } /// <summary> /// Gets or creates a CancellationTokenSource for a given guest-provided ID. /// If the ID already exists, returns the existing token. /// If the ID doesn't exist, creates a new CancellationTokenSource. /// </summary> /// <param name="tokenId">The token ID from the guest.</param> /// <returns>The CancellationToken for this ID.</returns> public CancellationToken GetOrCreate(string tokenId) { ObjectDisposedException.ThrowIf(_disposed, this); // Try to get existing first if (_sources.TryGetValue(tokenId, out var existing)) { return existing.Token; } // Create a new one var cts = new CancellationTokenSource(); // Use GetOrAdd to handle race conditions var added = _sources.GetOrAdd(tokenId, cts); if (added != cts) { // Another thread added first, dispose our copy cts.Dispose(); } return added.Token; } public void Dispose() { if (_disposed) { return; } _disposed = true; // Dispose all registered CancellationTokenSources foreach (var kvp in _sources) { try { kvp.Value.Dispose(); } catch { // Ignore disposal errors } } _sources.Clear(); } }