using System;
using System.Threading;
using System.Threading.Tasks;
using Deploymentcenter.Client;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core;
using PolyTrader.Core.Configuration;
using PolyTrader.Core.Security;
using PolyTraderSharp.Models;
namespace PolyTrader.Web.Updates
{
/// Ergebnis einer Update-Prüfung, wie das WebUI es anzeigt (#565).
public sealed record UpdateCheckSnapshot(
DateTime CheckedAtUtc,
bool Enabled,
bool UpdateAvailable,
bool Unauthorized,
string? LatestVersion,
string? Changelog,
bool IsCritical,
string? Error);
/// Fragt, ob es eine neuere Fassung gibt. Hinter einer Schnittstelle, damit sich die Zwischenspeicherung ohne Netz prüfen lässt.
public interface IUpdateChecker
{
Task CheckAsync(CancellationToken ct);
}
///
/// Hält das letzte Prüfergebnis vor (#565).
///
/// Ohne diesen Zwischenspeicher fragte jeder Seitenaufruf des WebUI das Deploymentcenter – bei
/// mehreren offenen Browserfenstern ein Mehrfaches dessen, was der Desktop mit einer Prüfung beim
/// Start braucht. "Jetzt prüfen" umgeht den Zwischenspeicher, aber frühestens nach
/// ; ein Doppelklick oder ein Skript in einer Schleife soll keine
/// Anfrageflut auslösen.
///
public sealed class UpdateStatusService
{
public static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
public static readonly TimeSpan MinManualInterval = TimeSpan.FromSeconds(60);
private readonly IUpdateChecker _checker;
private readonly Func _utcNow;
private readonly SemaphoreSlim _gate = new(1, 1);
private UpdateCheckSnapshot? _last;
public UpdateStatusService(IUpdateChecker checker, Func? utcNow = null)
{
_checker = checker;
_utcNow = utcNow ?? (() => DateTime.UtcNow);
}
/// Letztes Prüfergebnis, null vor der ersten Prüfung.
public UpdateCheckSnapshot? Last => _last;
///
/// Feuert nach jeder frischen Prüfung, nicht bei einer Antwort aus dem Zwischenspeicher. Der Desktop hängt
/// sein Update-Banner daran - geprüft wird an genau einer Stelle, angezeigt an zweien.
///
public event Action? Checked;
///
/// Liefert das Ergebnis, bei Bedarf frisch geprüft. RetryAfter ist gesetzt, wenn eine erzwungene
/// Prüfung zu früh kam und stattdessen der vorige Stand geliefert wird.
///
public async Task<(UpdateCheckSnapshot Snapshot, bool FromCache, TimeSpan? RetryAfter)> GetAsync(bool force, CancellationToken ct)
{
// Eine Prüfung zur Zeit: Gleichzeitige Aufrufe warten und bekommen danach das frische Ergebnis.
UpdateCheckSnapshot fresh;
await _gate.WaitAsync(ct);
try
{
var now = _utcNow();
if (_last != null)
{
var age = now - _last.CheckedAtUtc;
if (!force && age < CacheDuration)
return (_last, true, null);
if (force && age < MinManualInterval)
return (_last, true, MinManualInterval - age);
}
fresh = await _checker.CheckAsync(ct);
_last = fresh;
}
finally
{
_gate.Release();
}
// Ausserhalb der Sperre: Ein Empfaenger, der selbst GetAsync ruft, soll sich nicht selbst blockieren.
Checked?.Invoke(fresh);
return (fresh, false, null);
}
}
///
/// Prüft gegen das Deploymentcenter – derselbe Aufruf wie beim Start des Desktops (LauncherWindow).
/// Der Lizenzschlüssel für den Paketabruf wird hier gelesen und verlässt den Server nicht.
///
internal sealed class DeploymentcenterUpdateChecker : IUpdateChecker
{
private readonly IServiceProvider _services;
public DeploymentcenterUpdateChecker(IServiceProvider services) => _services = services;
public async Task CheckAsync(CancellationToken ct)
{
var now = DateTime.UtcNow;
var dc = _services.GetService();
if (dc == null || !dc.UpdateCheckEnabled || string.IsNullOrWhiteSpace(dc.BaseUrl))
return new UpdateCheckSnapshot(now, false, false, false, null, null, false,
"Die Update-Prüfung ist nicht eingerichtet (Deploymentcenter: UpdateCheckEnabled, BaseUrl).");
string? licenseKey = null;
try
{
var settings = _services.GetService()?.Read() ?? _services.GetService();
licenseKey = SecretProtection.Unprotect(settings?.LicenseKey ?? string.Empty);
}
catch
{
// Nicht lesbarer Schlüssel: Die Prüfung läuft ohne Zugangsdaten weiter, wie im Desktop.
}
try
{
var result = await new UpdateClient().CheckForUpdateAsync(
baseUrl: dc.BaseUrl,
projectId: dc.ProjectSlug,
currentVersion: BuildInfo.Version,
channel: dc.Channel,
credentials: ReleaseCredentials.FromLicenseKey(licenseKey));
if (result.Unauthorized)
return new UpdateCheckSnapshot(now, true, false, true, null, null, false,
"Die Lizenz trägt nicht für den Paketabruf.");
bool available = result.UpdateAvailable && result.LatestRelease != null;
return new UpdateCheckSnapshot(now, true, available, false,
result.LatestRelease?.Version, result.LatestRelease?.Changelog, available && result.IsCritical, null);
}
catch (Exception ex)
{
return new UpdateCheckSnapshot(now, true, false, false, null, null, false,
$"Deploymentcenter nicht erreichbar: {ex.Message}");
}
}
}
}