Editor/Server/HostSession.cs
#nullable enable annotations

using System;
using System.Threading;
using System.Threading.Tasks;

namespace HumanoidMocapHost;

/// <summary>Owns one HTTP listener, queue loop and file-store lifetime. Shutdown drains
/// uploads and GPU captures before disposing their resources.</summary>
internal sealed class HostSession : IAsyncDisposable
{
    readonly object gate = new();
    readonly CancellationTokenSource stopping = new();
    readonly FileJobStorage storage;
    readonly CaptureQueue queue;
    readonly HttpHost http;
    readonly Task worker;
    readonly Action<Exception> reportError;
    Task? shutdown;
    internal bool IsStopping { get { lock (gate) return shutdown is not null; } }

    internal HostSession(string prefix, ApiKeys keys, FileJobStorage storage, CaptureQueue queue, Action<Exception> reportError)
    {
        this.storage = storage; this.queue = queue; this.reportError = reportError;
        try { http = new HttpHost(prefix, keys, queue, reportError); }
        catch { stopping.Dispose(); queue.Dispose(); storage.Dispose(); throw; }
        worker = Task.Run(() => QueueLoop.RunAsync(queue, (id, error) => reportError(new InvalidOperationException($"Capture {id} failed.", error)), stopping.Token));
        _ = ObserveAsync();
    }

    async Task ObserveAsync()
    {
        try { await worker; }
        catch (Exception error) { reportError(error); }
        finally
        {
            try { await StopAsync(); }
            catch (Exception error) { reportError(error); }
        }
    }

    /// <summary>Idempotently stops this instance. The returned task completes only after
    /// cancellation and resource cleanup; callers must await it before starting a replacement.</summary>
    internal Task StopAsync()
    {
        lock (gate) return shutdown ??= StopCoreAsync();
    }

    async Task StopCoreAsync()
    {
        await stopping.CancelAsync();
        try { await http.DisposeAsync(); }
        finally
        {
            try { await worker; }
            finally
            {
                try { queue.Dispose(); }
                finally
                {
                    try { storage.DeleteRun(); }
                    finally { storage.Dispose(); stopping.Dispose(); }
                }
            }
        }
    }

    public ValueTask DisposeAsync() => new(StopAsync());
}