111 lines
3.4 KiB
C#
111 lines
3.4 KiB
C#
using Microsoft.Extensions.Logging;
|
||
|
||
namespace ClawdDotNet.App.Services;
|
||
|
||
/// <summary>
|
||
/// Prüft die Lizenz im laufenden Betrieb nach.
|
||
///
|
||
/// <para>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.</para>
|
||
///
|
||
/// <para><b>Nur ein Urteil zählt.</b> Ein Netzproblem, eine Drosselung oder eine
|
||
/// abgelaufene Offline-Frist beenden nichts — das SDK meldet solche Fälle als
|
||
/// <c>IsTransient</c>, und ein Serverausfall darf nicht alle laufenden Instanzen
|
||
/// mitnehmen. Der verschlüsselte Zwischenspeicher trägt über solche Lücken hinweg.</para>
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// Die Lizenz gilt nicht mehr. Der Aufrufer beendet die Anwendung — geordnet, aber
|
||
/// ohne Rückfrage; der übergebene Text erklärt den Grund.
|
||
/// </summary>
|
||
public event Func<string, Task>? 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();
|
||
}
|
||
}
|