feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user