using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Plex; namespace ErsatzTV.Application.Plex; public class TryCompletePlexPinFlowHandler : IRequestHandler> { private readonly ChannelWriter _channel; private readonly IEntityLocker _entityLocker; private readonly IPlexTvApiClient _plexTvApiClient; public TryCompletePlexPinFlowHandler( IPlexTvApiClient plexTvApiClient, ChannelWriter channel, IEntityLocker entityLocker) { _plexTvApiClient = plexTvApiClient; _channel = channel; _entityLocker = entityLocker; } public async Task> Handle(TryCompletePlexPinFlow request, CancellationToken cancellationToken) { // Lock-release discipline (#202 §C1.6 / §C6): the Plex lock this pin flow holds is released // ONLY on non-handoff exits — the 2-minute timeout (Task.Delay throws // OperationCanceledException), a poll exception, a failed enqueue, or the (effectively dead) // return-false at loop entry. On SUCCESS the lock is HANDED OFF to SynchronizePlexMediaSources, // whose handler is the sole releaser after server discovery (SynchronizePlexMediaSourcesHandler). // This is deliberately NOT an unconditional finally: a blanket release here would double-release // AND release before discovery, re-opening the finding-5 race (an empty server list reading as // success). Contrast the terminal SignOutOfPlexHandler, which DOES use finally (it has no handoff). using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken); CancellationToken token = linkedTokenSource.Token; try { while (!token.IsCancellationRequested) { bool result = await _plexTvApiClient.TryCompletePinFlow(request.AuthPin); if (result) { // hand the lock off to the sync handler — do NOT release on this success path await _channel.WriteAsync(new SynchronizePlexMediaSources(), token); return true; } await Task.Delay(TimeSpan.FromSeconds(1), token); } // effectively unreachable (Task.Delay throws on cancellation before the loop condition is // re-evaluated) but if the flow ever ends here it abandoned without auth → release _entityLocker.UnlockPlex(); return false; } catch (Exception) { // non-handoff exit: timeout-throw, poll exception, or failed enqueue — release the lock so an // abandoned flow does not wedge Plex locked, then rethrow (PlexService logs it as before) _entityLocker.UnlockPlex(); throw; } } }