// Copyright Epic Games, Inc. All Rights Reserved. using System.Diagnostics; using EpicGames.Core; using EpicGames.Horde; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace UnrealToolbox { /// /// Implements a mechanism for creating scoped HordeClient instances, and modifying configuration settings /// class HordeClientProvider : IHordeClientProvider, IAsyncDisposable { class HordeClientLifetime : IAsyncDisposable { readonly TaskCompletionSource _refZeroTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); int _refCount; ServiceProvider _serviceProvider; IHordeClient _hordeClient; public IHordeClient Client => _hordeClient; public HordeClientLifetime(ILoggerFactory loggerFactory) { _refCount = 1; ServiceCollection serviceCollection = new ServiceCollection(); serviceCollection.AddHorde(options => options.AllowAuthPrompt = false); serviceCollection.AddSingleton(loggerFactory); serviceCollection.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); _serviceProvider = serviceCollection.BuildServiceProvider(); _hordeClient = _serviceProvider.GetRequiredService(); } public async ValueTask DisposeAsync() { if (_serviceProvider != null) { Release(); await _refZeroTcs.Task; await _serviceProvider.DisposeAsync(); _serviceProvider = null!; _hordeClient = null!; } } public void AddRef() { int refCount = Interlocked.Increment(ref _refCount); Debug.Assert(refCount > 1); } public void Release() { if (Interlocked.Decrement(ref _refCount) == 0) { _refZeroTcs.SetResult(); } } } class HordeClientRef : IHordeClientRef { HordeClientLifetime? _lifetime; public IHordeClient Client => _lifetime?.Client ?? throw new ObjectDisposedException(null); public HordeClientRef(HordeClientLifetime lifetime) { _lifetime = lifetime; _lifetime.AddRef(); } public void Dispose() { _lifetime?.Release(); _lifetime = null; } } readonly object _lockObject = new object(); readonly ILogger _logger; readonly ILoggerFactory _loggerFactory; readonly List _disposeTasks = new List(); HordeClientLifetime? _lifetime; /// /// Event signalled whenever the connection state changes /// public event Action? OnStateChanged; /// /// Event signalled whenever the access token state changes /// public event Action? OnAccessTokenStateChanged; /// /// Constructor /// public HordeClientProvider(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); _loggerFactory = loggerFactory; CreateLifetime(); } /// public async ValueTask DisposeAsync() { if (_lifetime != null) { DestroyLifetime(); } await Task.WhenAll(_disposeTasks); } /// public IHordeClientRef? GetClientRef() { lock (_lockObject) { if (_lifetime == null) { return null; } else { return new HordeClientRef(_lifetime); } } } /// public void Reset() { lock (_lockObject) { DestroyLifetime(); CreateLifetime(); } OnStateChanged?.Invoke(); } void OnAccessTokenStateChangedInternal() { OnAccessTokenStateChanged?.Invoke(); } void CreateLifetime() { Debug.Assert(_lifetime == null); try { _lifetime = new HordeClientLifetime(_loggerFactory); _lifetime.Client.OnAccessTokenStateChanged += OnAccessTokenStateChangedInternal; } catch (Exception ex) { _logger.LogError(ex, "Unable to create Horde client lifetime: {Message}", ex.Message); _lifetime = null; } } void DestroyLifetime() { if (_lifetime != null) { _lifetime.Client.OnAccessTokenStateChanged -= OnAccessTokenStateChangedInternal; _disposeTasks.Add(_lifetime.DisposeAsync().AsTask()); _lifetime = null; } AsyncUtils.RemoveCompleteTasks(_disposeTasks); } } }