Update-Pruefung regelmaessig und ab Werk an, Installation nur auf Klick
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>
This commit is contained in:
@@ -368,6 +368,9 @@ namespace PolyTrader.App.Avalonia
|
|||||||
services.AddHostedService(sp => sp.GetRequiredService<WatchdogHeartbeatService>());
|
services.AddHostedService(sp => sp.GetRequiredService<WatchdogHeartbeatService>());
|
||||||
services.AddHostedService(sp => sp.GetRequiredService<DeploymentcenterErrorReporter>());
|
services.AddHostedService(sp => sp.GetRequiredService<DeploymentcenterErrorReporter>());
|
||||||
|
|
||||||
|
// #587: regelmäßige Update-Prüfung mit Hinweis - installiert nie selbst.
|
||||||
|
PolyTrader.Web.Updates.UpdateCheckServiceCollectionExtensions.AddPolyTraderUpdateCheck(services);
|
||||||
|
|
||||||
// Shell-Zustand (Fensterverwaltung). Auch im kopflosen Betrieb harmlos.
|
// Shell-Zustand (Fensterverwaltung). Auch im kopflosen Betrieb harmlos.
|
||||||
services.AddSingleton<AvaloniaUiHost>();
|
services.AddSingleton<AvaloniaUiHost>();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using PolyTrader.Core.Modularity;
|
|||||||
using PolyTrader.Core.Persistence;
|
using PolyTrader.Core.Persistence;
|
||||||
using PolyTrader.Core.Security;
|
using PolyTrader.Core.Security;
|
||||||
using PolyTrader.Modules.Supervisor.Persistence;
|
using PolyTrader.Modules.Supervisor.Persistence;
|
||||||
|
using PolyTrader.Web.Updates;
|
||||||
using PolyTraderSharp;
|
using PolyTraderSharp;
|
||||||
using PolyTraderSharp.Models;
|
using PolyTraderSharp.Models;
|
||||||
using PolyTraderSharp.Services;
|
using PolyTraderSharp.Services;
|
||||||
@@ -51,8 +52,11 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
|
|
||||||
private int _statusTicks;
|
private int _statusTicks;
|
||||||
|
|
||||||
/// <summary>Ergebnis der letzten Update-Prüfung – für den Klick auf „Jetzt installieren …".</summary>
|
/// <summary>Fassung aus der letzten Update-Prüfung – für den Klick auf „Jetzt installieren …".</summary>
|
||||||
private UpdateCheckResult? _updateCheck;
|
private string? _updateVersion;
|
||||||
|
|
||||||
|
/// <summary>Mit „Später" ausgeblendete Fassung – das Banner kommt erst für eine noch neuere wieder.</summary>
|
||||||
|
private string? _dismissedVersion;
|
||||||
|
|
||||||
public LauncherWindow() => AvaloniaXamlLoader.Load(this);
|
public LauncherWindow() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
@@ -84,16 +88,25 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
|
|
||||||
this.FindControl<Button>("btnInstallUpdate")!.Click += async (_, _) => await InstallUpdateAsync();
|
this.FindControl<Button>("btnInstallUpdate")!.Click += async (_, _) => await InstallUpdateAsync();
|
||||||
this.FindControl<Button>("btnDismissUpdate")!.Click += (_, _) =>
|
this.FindControl<Button>("btnDismissUpdate")!.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
_dismissedVersion = _updateVersion;
|
||||||
this.FindControl<Border>("pnlUpdateBanner")!.IsVisible = false;
|
this.FindControl<Border>("pnlUpdateBanner")!.IsVisible = false;
|
||||||
|
};
|
||||||
|
|
||||||
_statusTimer.Tick += (_, _) => UpdateStatus();
|
_statusTimer.Tick += (_, _) => UpdateStatus();
|
||||||
_statusTimer.Start();
|
_statusTimer.Start();
|
||||||
UpdateStatus();
|
UpdateStatus();
|
||||||
RefreshData();
|
RefreshData();
|
||||||
|
|
||||||
// D-4: einmalige, stille Prüfung beim Start - fire-and-forget, darf den Start nie
|
// #587: Geprüft wird regelmäßig im Hintergrund (UpdateCheckBackgroundService), hier nur angezeigt.
|
||||||
// verzögern oder blockieren. Nur aktiv, wenn Deploymentcenter:UpdateCheckEnabled gesetzt ist.
|
// Bis zum 14.09.2026 prüfte diese Seite genau einmal beim Start - eine wochenlang laufende
|
||||||
_ = CheckForUpdateAsync();
|
// Anwendung erfuhr nie von einem neuen Release. Kein Abmelden, siehe ThemeChanged unten.
|
||||||
|
var updates = services.GetService<UpdateStatusService>();
|
||||||
|
if (updates != null)
|
||||||
|
{
|
||||||
|
updates.Checked += OnUpdateChecked;
|
||||||
|
if (updates.Last is { } last) OnUpdateChecked(last);
|
||||||
|
}
|
||||||
|
|
||||||
// Die Umschalterflaechen werden im Code gesetzt und folgen dem Thema nicht von selbst.
|
// Die Umschalterflaechen werden im Code gesetzt und folgen dem Thema nicht von selbst.
|
||||||
// Kein Abmelden: die Seite lebt in der Einfenster-Shell bis zum Programmende, ein
|
// Kein Abmelden: die Seite lebt in der Einfenster-Shell bis zum Programmende, ein
|
||||||
@@ -185,56 +198,29 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
// ===== Update-Prüfung (D-4) =====
|
// ===== Update-Prüfung (D-4) =====
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prüft beim Start still auf ein neues Release. Installiert NIE automatisch (Leitentscheidung
|
/// Zeigt das Ergebnis einer Hintergrundprüfung an. Installiert NIE automatisch (Leitentscheidung 4,
|
||||||
/// 4 aus dem Umsetzungsplan) - ein `is_critical`-Release ändert nur den Hinweistext, nicht das
|
/// bestätigt am 14.09.2026) - ein `is_critical`-Release ändert nur den Hinweistext, nicht das Verhalten.
|
||||||
/// Verhalten. Wirft nie: ein nicht erreichbares Deploymentcenter darf den Start nicht stören.
|
/// Kein Update, Fehler oder fehlende Lizenz blenden das Banner aus; die Einzelheiten stehen im WebUI
|
||||||
|
/// unter Einstellungen → Version & Updates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CheckForUpdateAsync()
|
private void OnUpdateChecked(UpdateCheckSnapshot snapshot)
|
||||||
{
|
{
|
||||||
try
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
var dcOptions = _services.GetService<DeploymentcenterOptions>();
|
var banner = this.FindControl<Border>("pnlUpdateBanner")!;
|
||||||
if (dcOptions == null || !dcOptions.UpdateCheckEnabled || string.IsNullOrWhiteSpace(dcOptions.BaseUrl))
|
if (!snapshot.UpdateAvailable || string.IsNullOrEmpty(snapshot.LatestVersion))
|
||||||
return;
|
|
||||||
|
|
||||||
var settings = _services.GetService<ServerSettings>();
|
|
||||||
string? licenseKey = null;
|
|
||||||
try { licenseKey = SecretProtection.Unprotect(settings?.LicenseKey ?? string.Empty); }
|
|
||||||
catch { /* nicht lesbarer Schlüssel -> Prüfung läuft ohne Zugangsdaten weiter */ }
|
|
||||||
|
|
||||||
var updateClient = new UpdateClient();
|
|
||||||
var result = await updateClient.CheckForUpdateAsync(
|
|
||||||
baseUrl: dcOptions.BaseUrl,
|
|
||||||
projectId: dcOptions.ProjectSlug,
|
|
||||||
currentVersion: BuildInfo.Version,
|
|
||||||
channel: dcOptions.Channel,
|
|
||||||
credentials: ReleaseCredentials.FromLicenseKey(licenseKey));
|
|
||||||
|
|
||||||
if (result.Unauthorized)
|
|
||||||
{
|
{
|
||||||
// Kein Netzwerkfehler: die Lizenz trägt nicht mehr für den Paketabruf. Kein
|
_updateVersion = null;
|
||||||
// Alarm im Launcher nötig - die Lizenzprüfung selbst (D-2) hat das schon gemeldet.
|
banner.IsVisible = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.UpdateAvailable || result.LatestRelease == null) return;
|
_updateVersion = snapshot.LatestVersion;
|
||||||
|
string critical = snapshot.IsCritical ? " (empfohlenes Sicherheitsupdate)" : "";
|
||||||
_updateCheck = result;
|
this.FindControl<TextBlock>("lblUpdateInfo")!.Text =
|
||||||
string version = result.LatestRelease.Version;
|
$"Update verfügbar: v{snapshot.LatestVersion}{critical}. {snapshot.Changelog}".TrimEnd();
|
||||||
string critical = result.IsCritical ? " (empfohlenes Sicherheitsupdate)" : "";
|
banner.IsVisible = !string.Equals(_dismissedVersion, snapshot.LatestVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
this.FindControl<TextBlock>("lblUpdateInfo")!.Text =
|
|
||||||
$"Update verfügbar: v{version}{critical}. {result.LatestRelease.Changelog}".TrimEnd();
|
|
||||||
this.FindControl<Border>("pnlUpdateBanner")!.IsVisible = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Update-Prüfung darf den Start nie stören - stiller Rückzug, kein Log-Spam beim
|
|
||||||
// vermutlich häufigsten Fall (Deploymentcenter nicht erreichbar).
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -244,7 +230,7 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task InstallUpdateAsync()
|
private async Task InstallUpdateAsync()
|
||||||
{
|
{
|
||||||
if (_updateCheck?.LatestRelease == null) return;
|
if (_updateVersion == null) return;
|
||||||
|
|
||||||
var dcOptions = _services.GetService<DeploymentcenterOptions>();
|
var dcOptions = _services.GetService<DeploymentcenterOptions>();
|
||||||
if (dcOptions == null) return;
|
if (dcOptions == null) return;
|
||||||
@@ -259,7 +245,7 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool confirmed = await DialogWindow.Confirm(this, "Update installieren",
|
bool confirmed = await DialogWindow.Confirm(this, "Update installieren",
|
||||||
$"Version {_updateCheck.LatestRelease.Version} herunterladen und PolyTrader danach " +
|
$"Version {_updateVersion} herunterladen und PolyTrader danach " +
|
||||||
"neu starten?\n\nOffene Vorgänge werden dabei genauso geordnet beendet wie über „Beenden\".",
|
"neu starten?\n\nOffene Vorgänge werden dabei genauso geordnet beendet wie über „Beenden\".",
|
||||||
"Installieren");
|
"Installieren");
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
"WatchdogIntervalSeconds": 60,
|
"WatchdogIntervalSeconds": 60,
|
||||||
"WatchdogEnabled": false,
|
"WatchdogEnabled": false,
|
||||||
"ErrorReportingEnabled": false,
|
"ErrorReportingEnabled": false,
|
||||||
"UpdateCheckEnabled": false
|
"_UpdateCheck_comment": "Prueft regelmaessig auf ein neues Release und weist darauf hin. Installiert wird nie automatisch, nur auf Klick im Desktop.",
|
||||||
|
"UpdateCheckEnabled": true,
|
||||||
|
"UpdateCheckIntervalHours": 6
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
|
|||||||
@@ -54,7 +54,14 @@ namespace PolyTrader.Core.Configuration
|
|||||||
/// <summary>Ungefangene Fehler an /api/errors/v1/report melden.</summary>
|
/// <summary>Ungefangene Fehler an /api/errors/v1/report melden.</summary>
|
||||||
public bool ErrorReportingEnabled { get; set; } = false;
|
public bool ErrorReportingEnabled { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>Beim Start auf ein neues Release prüfen (installiert nie automatisch).</summary>
|
/// <summary>
|
||||||
public bool UpdateCheckEnabled { get; set; } = false;
|
/// Regelmäßig auf ein neues Release prüfen und darauf hinweisen. Installiert nie automatisch - das
|
||||||
|
/// geschieht nur auf Klick im Desktop. Ab Werk an (Richard, 14.09.2026, #587): Vorher stand hier
|
||||||
|
/// <c>false</c>, und eine frisch installierte Anwendung erfuhr nie von einem neuen Release.
|
||||||
|
/// </summary>
|
||||||
|
public bool UpdateCheckEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Abstand der Update-Prüfungen in Stunden, 1 bis 168.</summary>
|
||||||
|
public int UpdateCheckIntervalHours { get; set; } = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ namespace PolyTrader.Web.Hosting
|
|||||||
{
|
{
|
||||||
services.AddSingleton(Options.Create(options));
|
services.AddSingleton(Options.Create(options));
|
||||||
|
|
||||||
// #565: Update-Status mit Zwischenspeicher - nicht jeder Seitenaufruf fragt das Deploymentcenter.
|
// Update-Status (#565) registriert Program.cs über AddPolyTraderUpdateCheck - unabhängig vom WebUI,
|
||||||
services.AddSingleton<IUpdateChecker, DeploymentcenterUpdateChecker>();
|
// damit die regelmäßige Prüfung (#587) auch ohne WebUI läuft.
|
||||||
services.AddSingleton(sp => new UpdateStatusService(sp.GetRequiredService<IUpdateChecker>()));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
hostBuilder.ConfigureWebHostDefaults(webBuilder =>
|
hostBuilder.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using PolyTrader.Core.Configuration;
|
||||||
|
using PolyTraderSharp.Services;
|
||||||
|
|
||||||
|
namespace PolyTrader.Web.Updates
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Prüft regelmäßig auf ein neues Release und weist darauf hin (#587).
|
||||||
|
///
|
||||||
|
/// <para><b>Installiert nie.</b> Richards Vorgabe vom 14.09.2026: Updates werden nicht automatisch
|
||||||
|
/// eingespielt, die Prüfung mit Hinweis läuft aber von selbst. Installiert wird nur auf Klick im Desktop
|
||||||
|
/// ("Jetzt installieren …"), weil das die Anwendung beendet.</para>
|
||||||
|
///
|
||||||
|
/// <para>Bis dahin prüfte der Desktop genau einmal beim Start und das WebUI nur, wenn jemand die Seite
|
||||||
|
/// aufrief. Eine Anwendung, die wochenlang läuft - der Normalfall im Betrieb -, erfuhr so nie von einem
|
||||||
|
/// neuen Release. Der Dienst läuft auch ohne WebUI und im kopflosen Betrieb; das Ergebnis landet im
|
||||||
|
/// gemeinsamen <see cref="UpdateStatusService"/>, an dem Desktop-Banner und WebUI hängen.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateCheckBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
/// <summary>Erste Prüfung kurz nach dem Start - nicht sofort, damit sie den Start nicht belastet.</summary>
|
||||||
|
public static readonly TimeSpan StartDelay = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
private readonly UpdateStatusService _status;
|
||||||
|
private readonly TerminalLogger _logger;
|
||||||
|
private readonly TimeSpan _interval;
|
||||||
|
private string? _announcedVersion;
|
||||||
|
|
||||||
|
public UpdateCheckBackgroundService(UpdateStatusService status, TerminalLogger logger, DeploymentcenterOptions? options)
|
||||||
|
{
|
||||||
|
_status = status;
|
||||||
|
_logger = logger;
|
||||||
|
_interval = Interval(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Takt aus <see cref="DeploymentcenterOptions.UpdateCheckIntervalHours"/>, auf 1 bis 168 Stunden begrenzt.</summary>
|
||||||
|
public static TimeSpan Interval(DeploymentcenterOptions? options) =>
|
||||||
|
TimeSpan.FromHours(Math.Clamp(options?.UpdateCheckIntervalHours ?? 6, 1, 168));
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(StartDelay, stoppingToken).ConfigureAwait(false);
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await CheckOnceAsync(stoppingToken).ConfigureAwait(false);
|
||||||
|
await Task.Delay(_interval, stoppingToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Herunterfahren
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eine Prüfung. Der Hinweis im Log erscheint einmal je neuer Fassung, nicht bei jeder Prüfung - sonst
|
||||||
|
/// stünde dieselbe Zeile alle paar Stunden im Log, bis jemand installiert.
|
||||||
|
/// </summary>
|
||||||
|
public async Task CheckOnceAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var (snapshot, _, _) = await _status.GetAsync(force: false, ct).ConfigureAwait(false);
|
||||||
|
if (!snapshot.UpdateAvailable || string.IsNullOrEmpty(snapshot.LatestVersion)) return;
|
||||||
|
if (string.Equals(snapshot.LatestVersion, _announcedVersion, StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
|
||||||
|
_announcedVersion = snapshot.LatestVersion;
|
||||||
|
string critical = snapshot.IsCritical ? " (empfohlenes Sicherheitsupdate)" : "";
|
||||||
|
_logger.Info($"🔔 Update verfügbar: v{snapshot.LatestVersion}{critical}. Installiert wird nur auf Wunsch - " +
|
||||||
|
"im Desktop über „Jetzt installieren …“ auf der Startseite.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UpdateCheckServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Update-Prüfung für Desktop, WebUI und kopflosen Betrieb. Unabhängig davon, ob das WebUI läuft - bis
|
||||||
|
/// zum 14.09.2026 hing die Registrierung am WebUI, ohne WebUI gab es keine regelmäßige Prüfung.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddPolyTraderUpdateCheck(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IUpdateChecker, DeploymentcenterUpdateChecker>();
|
||||||
|
services.AddSingleton(sp => new UpdateStatusService(sp.GetRequiredService<IUpdateChecker>()));
|
||||||
|
services.AddHostedService(sp => new UpdateCheckBackgroundService(
|
||||||
|
sp.GetRequiredService<UpdateStatusService>(),
|
||||||
|
sp.GetRequiredService<TerminalLogger>(),
|
||||||
|
sp.GetService<DeploymentcenterOptions>()));
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,15 @@ namespace PolyTrader.Web.Updates
|
|||||||
_utcNow = utcNow ?? (() => DateTime.UtcNow);
|
_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>
|
/// <summary>
|
||||||
/// Liefert das Ergebnis, bei Bedarf frisch geprüft. <c>RetryAfter</c> ist gesetzt, wenn eine erzwungene
|
/// 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.
|
/// Prüfung zu früh kam und stattdessen der vorige Stand geliefert wird.
|
||||||
@@ -59,6 +68,7 @@ namespace PolyTrader.Web.Updates
|
|||||||
public async Task<(UpdateCheckSnapshot Snapshot, bool FromCache, TimeSpan? RetryAfter)> GetAsync(bool force, CancellationToken ct)
|
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.
|
// Eine Prüfung zur Zeit: Gleichzeitige Aufrufe warten und bekommen danach das frische Ergebnis.
|
||||||
|
UpdateCheckSnapshot fresh;
|
||||||
await _gate.WaitAsync(ct);
|
await _gate.WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -72,13 +82,17 @@ namespace PolyTrader.Web.Updates
|
|||||||
return (_last, true, MinManualInterval - age);
|
return (_last, true, MinManualInterval - age);
|
||||||
}
|
}
|
||||||
|
|
||||||
_last = await _checker.CheckAsync(ct);
|
fresh = await _checker.CheckAsync(ct);
|
||||||
return (_last, false, null);
|
_last = fresh;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_gate.Release();
|
_gate.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ausserhalb der Sperre: Ein Empfaenger, der selbst GetAsync ruft, soll sich nicht selbst blockieren.
|
||||||
|
Checked?.Invoke(fresh);
|
||||||
|
return (fresh, false, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
applyThemeChoice(currentThemeChoice());
|
applyThemeChoice(currentThemeChoice());
|
||||||
supChatWelcome = document.getElementById('supChatMessages')?.innerHTML ?? '';
|
supChatWelcome = document.getElementById('supChatMessages')?.innerHTML ?? '';
|
||||||
loadUpdateStatus(false); // #565: Banner im Dashboard; der Server haelt das Ergebnis vor
|
loadUpdateStatus(false); // #565: Banner im Dashboard; der Server haelt das Ergebnis vor
|
||||||
|
// #587: Der Server prueft regelmaessig selbst; die Seite holt nur den vorgehaltenen Stand nach,
|
||||||
|
// damit ein lange offenes Browserfenster ein neues Release auch mitbekommt.
|
||||||
|
setInterval(() => loadUpdateStatus(false), 10 * 60 * 1000);
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Escape' && document.getElementById('modalStopTrading')?.style.display === 'flex') closeStopTradingModal();
|
if (e.key === 'Escape' && document.getElementById('modalStopTrading')?.style.display === 'flex') closeStopTradingModal();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -104,6 +104,80 @@ namespace PolyTrader.Tests.WebUi
|
|||||||
Assert.Equal(1, checker.Calls);
|
Assert.Equal(1, checker.Calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Checked_feuert_nur_bei_frischer_Pruefung()
|
||||||
|
{
|
||||||
|
var clock = new Clock();
|
||||||
|
var checker = new FakeChecker(() => clock.Now);
|
||||||
|
var service = new UpdateStatusService(checker, () => clock.Now);
|
||||||
|
int events = 0;
|
||||||
|
service.Checked += _ => events++;
|
||||||
|
|
||||||
|
await service.GetAsync(false, CancellationToken.None);
|
||||||
|
await service.GetAsync(false, CancellationToken.None); // aus dem Zwischenspeicher
|
||||||
|
|
||||||
|
Assert.Equal(1, events);
|
||||||
|
Assert.Equal("0.2.0", service.Last?.LatestVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Hintergrundpruefung_meldet_jede_neue_Fassung_genau_einmal_und_installiert_nichts()
|
||||||
|
{
|
||||||
|
var clock = new Clock();
|
||||||
|
var checker = new FakeChecker(() => clock.Now);
|
||||||
|
var status = new UpdateStatusService(checker, () => clock.Now);
|
||||||
|
var logger = new PolyTraderSharp.Services.TerminalLogger();
|
||||||
|
var notices = new System.Collections.Generic.List<string>();
|
||||||
|
logger.OnLogMessage += (_, e) => { if (e.Message.Contains("Update verfügbar")) notices.Add(e.Message); };
|
||||||
|
var background = new UpdateCheckBackgroundService(status, logger, null);
|
||||||
|
|
||||||
|
await background.CheckOnceAsync(CancellationToken.None);
|
||||||
|
clock.Now = clock.Now + UpdateStatusService.CacheDuration;
|
||||||
|
await background.CheckOnceAsync(CancellationToken.None); // dieselbe Fassung: kein zweiter Hinweis
|
||||||
|
|
||||||
|
checker.Result = checker.Result with { LatestVersion = "0.3.0" };
|
||||||
|
clock.Now = clock.Now + UpdateStatusService.CacheDuration;
|
||||||
|
await background.CheckOnceAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(2, notices.Count);
|
||||||
|
Assert.Contains("v0.2.0", notices[0]);
|
||||||
|
Assert.Contains("v0.3.0", notices[1]);
|
||||||
|
Assert.All(notices, n => Assert.Contains("nur auf Wunsch", n));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Ohne_Update_kein_Hinweis()
|
||||||
|
{
|
||||||
|
var clock = new Clock();
|
||||||
|
var checker = new FakeChecker(() => clock.Now);
|
||||||
|
checker.Result = checker.Result with { UpdateAvailable = false, LatestVersion = null };
|
||||||
|
var logger = new PolyTraderSharp.Services.TerminalLogger();
|
||||||
|
int notices = 0;
|
||||||
|
logger.OnLogMessage += (_, e) => { if (e.Message.Contains("Update verfügbar")) notices++; };
|
||||||
|
|
||||||
|
await new UpdateCheckBackgroundService(new UpdateStatusService(checker, () => clock.Now), logger, null)
|
||||||
|
.CheckOnceAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(0, notices);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, 6)]
|
||||||
|
[InlineData(0, 1)]
|
||||||
|
[InlineData(12, 12)]
|
||||||
|
[InlineData(10000, 168)]
|
||||||
|
public void Pruefintervall_ist_begrenzt(int? hours, int expected)
|
||||||
|
{
|
||||||
|
var options = hours == null ? null : new PolyTrader.Core.Configuration.DeploymentcenterOptions { UpdateCheckIntervalHours = hours.Value };
|
||||||
|
Assert.Equal(TimeSpan.FromHours(expected), UpdateCheckBackgroundService.Interval(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_Pruefung_ist_ab_Werk_an()
|
||||||
|
{
|
||||||
|
Assert.True(new PolyTrader.Core.Configuration.DeploymentcenterOptions().UpdateCheckEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Route_liefert_Fassung_Stand_und_verweist_zur_Installation_auf_den_Desktop()
|
public async Task Route_liefert_Fassung_Stand_und_verweist_zur_Installation_auf_den_Desktop()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user