Richards Vorgabe vom 14.09.2026 (#587): Updates nie automatisch installieren, aber regelmaessig pruefen und darauf hinweisen. Bisher pruefte der Desktop genau einmal beim Start, das WebUI nur beim Seitenaufruf, und ab Werk gar nicht (UpdateCheckEnabled=false). Eine wochenlang laufende oder frisch installierte Anwendung erfuhr nie von einem neuen Release. - UpdateCheckEnabled ab Werk true, neu UpdateCheckIntervalHours (6, 1..168) - UpdateCheckBackgroundService: erste Pruefung nach 30 s, dann im Takt; laeuft auch ohne WebUI und kopflos; Hinweis im Log einmal je neuer Fassung - Registrierung ueber AddPolyTraderUpdateCheck, nicht mehr am WebUI - Desktop-Banner haengt an UpdateStatusService.Checked; "Spaeter" blendet bis zur naechsten neueren Fassung aus - WebUI-Banner holt alle 10 Minuten den vorgehaltenen Stand Auf der Linux-VM geprueft: 0.1.9 ohne Handeinstellung erkennt 0.2.0, ein Hinweis im Log, Banner erscheint, "Spaeter" blendet aus, Beenden in 0,1 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 lines
6.4 KiB
C#
153 lines
6.4 KiB
C#
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
|
||
{
|
||
/// <summary>Ergebnis einer Update-Prüfung, wie das WebUI es anzeigt (#565).</summary>
|
||
public sealed record UpdateCheckSnapshot(
|
||
DateTime CheckedAtUtc,
|
||
bool Enabled,
|
||
bool UpdateAvailable,
|
||
bool Unauthorized,
|
||
string? LatestVersion,
|
||
string? Changelog,
|
||
bool IsCritical,
|
||
string? Error);
|
||
|
||
/// <summary>Fragt, ob es eine neuere Fassung gibt. Hinter einer Schnittstelle, damit sich die Zwischenspeicherung ohne Netz prüfen lässt.</summary>
|
||
public interface IUpdateChecker
|
||
{
|
||
Task<UpdateCheckSnapshot> CheckAsync(CancellationToken ct);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Hält das letzte Prüfergebnis vor (#565).
|
||
///
|
||
/// <para>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
|
||
/// <see cref="MinManualInterval"/>; ein Doppelklick oder ein Skript in einer Schleife soll keine
|
||
/// Anfrageflut auslösen.</para>
|
||
/// </summary>
|
||
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<DateTime> _utcNow;
|
||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||
private UpdateCheckSnapshot? _last;
|
||
|
||
public UpdateStatusService(IUpdateChecker checker, Func<DateTime>? utcNow = null)
|
||
{
|
||
_checker = checker;
|
||
_utcNow = utcNow ?? (() => DateTime.UtcNow);
|
||
}
|
||
|
||
/// <summary>Letztes Prüfergebnis, <c>null</c> vor der ersten Prüfung.</summary>
|
||
public UpdateCheckSnapshot? Last => _last;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public event Action<UpdateCheckSnapshot>? Checked;
|
||
|
||
/// <summary>
|
||
/// Liefert das Ergebnis, bei Bedarf frisch geprüft. <c>RetryAfter</c> ist gesetzt, wenn eine erzwungene
|
||
/// Prüfung zu früh kam und stattdessen der vorige Stand geliefert wird.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
internal sealed class DeploymentcenterUpdateChecker : IUpdateChecker
|
||
{
|
||
private readonly IServiceProvider _services;
|
||
|
||
public DeploymentcenterUpdateChecker(IServiceProvider services) => _services = services;
|
||
|
||
public async Task<UpdateCheckSnapshot> CheckAsync(CancellationToken ct)
|
||
{
|
||
var now = DateTime.UtcNow;
|
||
var dc = _services.GetService<DeploymentcenterOptions>();
|
||
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<ServerSettingsFile>()?.Read() ?? _services.GetService<ServerSettings>();
|
||
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}");
|
||
}
|
||
}
|
||
}
|
||
}
|