using Microsoft.Extensions.Logging; namespace ClawdDotNet.App.Services; /// /// Prüft die Lizenz im laufenden Betrieb nach. /// /// Ohne das wirkt ein Widerruf erst beim nächsten Start — bei einer Anwendung, die /// als Dienst wochenlang läuft, ist das praktisch nie. Der Takt ist bewusst grob (alle /// zwölf Stunden): Es geht um einen Notausschalter, nicht um eine Zugangskontrolle pro /// Klick. /// /// Nur ein Urteil zählt. Ein Netzproblem, eine Drosselung oder eine /// abgelaufene Offline-Frist beenden nichts — das SDK meldet solche Fälle als /// IsTransient, und ein Serverausfall darf nicht alle laufenden Instanzen /// mitnehmen. Der verschlüsselte Zwischenspeicher trägt über solche Lücken hinweg. /// public sealed class LicenseWatch : IAsyncDisposable { private static readonly TimeSpan DefaultInterval = TimeSpan.FromHours(12); private readonly LicenseGate _gate; private readonly ILogger _logger; private readonly TimeSpan _interval; private CancellationTokenSource? _cts; private Task? _loop; /// /// Die Lizenz gilt nicht mehr. Der Aufrufer beendet die Anwendung — geordnet, aber /// ohne Rückfrage; der übergebene Text erklärt den Grund. /// public event Func? Revoked; public LicenseWatch(LicenseGate gate, ILogger logger, TimeSpan? interval = null) { _gate = gate; _logger = logger; _interval = interval ?? DefaultInterval; } public void Start() { if (_loop is { IsCompleted: false }) return; _cts = new CancellationTokenSource(); _loop = RunAsync(_cts.Token); } private async Task RunAsync(CancellationToken ct) { try { using var timer = new PeriodicTimer(_interval); while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false)) { var result = await _gate.RevalidateAsync(ct).ConfigureAwait(false); if (result.IsValid) continue; if (result.IsTransient) { _logger.LogInformation( "Lizenz-Nachprüfung ohne Ergebnis ({Status}) – Betrieb läuft weiter.", result.Status); continue; } _logger.LogWarning("Lizenz gilt nicht mehr ({Status}) – Instanz wird beendet.", result.Status); if (Revoked is { } handler) { await handler($"Die Lizenz ist nicht mehr gültig ({result.Status}). " + "ClawdDotNet wird beendet.").ConfigureAwait(false); } return; } } catch (OperationCanceledException) { // Regulärer Stopp. } catch (Exception ex) { // Die Nachprüfung darf den Betrieb nicht mitnehmen. _logger.LogWarning(ex, "Lizenz-Nachprüfung abgebrochen."); } } public async ValueTask DisposeAsync() { if (_cts is null) return; await _cts.CancelAsync().ConfigureAwait(false); if (_loop is not null) { try { await _loop.ConfigureAwait(false); } catch (OperationCanceledException) { /* erwartet */ } } _cts.Dispose(); } }