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}"); } } } }