WinForms-Host durch die Avalonia-Shell abgeloest
Merge von main: die dort entstandene Deployment-Center-Integration (Lizenz, Heartbeat mit DB-Health-Check, Fehler-Stream, UpdateService) ist jetzt Teil des plattformneutralen Kerns. Predictalytics.WinFormsHost ist entfernt. Nach Predictalytics.Hosting gezogen: - DcConfig, DcApiClient, DcErrorSink, DcHeartbeatService, DcUpdateService unveraendert - sie waren bereits plattformneutral - DcErrorReporter ohne Application.ThreadException und MessageBox; der UI-Thread-Handler liegt jetzt beim Host und ruft ReportUiThreadException - LicenseGuard/LicenseSession ohne Dialog und ohne WinForms-Timer. Neu: TryUseCachedAsync, ActivateAsync, StartPeriodicRevalidation ueber PeriodicTimer. Die Unterscheidung transienter Fehler und die Warnung vor ablaufender Gnadenfrist sind unveraendert uebernommen. - Dc-Einstellungen von AppSettings nach PredictalyticsOptions; die Watchdog-Einstellungen entfallen - DcErrorSink im LoggingSetup, Startbanner nutzt DcConfig.AppVersion BuildInfo.targets wird jetzt von Predictalytics.Hosting importiert. In der Avalonia-Shell nachgezogen: - Menue Deployment Center mit Update-Suche und Lizenzstatus - Einstellungsgruppe Deployment Center statt Watchdog, Update-Kanal als ComboBox, Server-URL nur zur Anzeige - Heartbeat-Snapshot mit SELECT-1-Probe wie in der WinForms-Fassung - Update-Pruefung still beim Start und interaktiv ueber das Menue, mit NotifyStopping vor dem Start des Update-Agenten - Lizenzfenster wertet IsTransient aus: bei fehlender Serververbindung wird nicht behauptet, die Lizenz sei ungueltig - TextBox.Watermark auf PlaceholderText (in Avalonia 12 veraltet) Build: 0 Fehler, 8 Warnungen (alle vorbestehend). Tests: 100 bestanden, 0 Fehler, 1 uebersprungen. Verifiziert: --license-status meldet gueltig samt Gnadenfrist; die GUI startet durch, prueft die Lizenz und laeuft gegen den UpdateService. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,25 +24,32 @@ internal static class HeadlessRunner
|
||||
|
||||
// Kein Terminal-Sink: Ausgabe geht auf die Konsole und in die Logdateien.
|
||||
LoggingSetup.Configure();
|
||||
|
||||
DcErrorReporter.Configure(options.DcToken, options.DcErrorReportingEnabled);
|
||||
DcErrorReporter.InstallGlobalHandlers();
|
||||
Deploymentcenter.Client.LicenseClient.DefaultAppVersion = DcConfig.AppVersion;
|
||||
|
||||
LoggingSetup.LogStartupBanner();
|
||||
Log.Information("Headless-Modus. Einstellungen: {Path}", PredictalyticsOptions.SettingsFilePath);
|
||||
|
||||
DcHeartbeatService? heartbeat = null;
|
||||
try
|
||||
{
|
||||
if (!await EnsureLicensedAsync()) return ExitNoLicense;
|
||||
var session = await EnsureLicensedAsync();
|
||||
if (session is null) return ExitNoLicense;
|
||||
|
||||
var host = new PredictalyticsHost(options);
|
||||
using var cts = new CancellationTokenSource();
|
||||
using var shutdownSignals = RegisterShutdown(cts);
|
||||
|
||||
using var licenseWatch = LicenseGuard.StartPeriodicRevalidation(result =>
|
||||
using var licenseWatch = LicenseGuard.StartPeriodicRevalidation(session, result =>
|
||||
{
|
||||
Log.Fatal("Lizenz nicht mehr gültig ({Status}): {Message} — Dienst wird beendet.",
|
||||
result.Status, result.Message);
|
||||
cts.Cancel();
|
||||
});
|
||||
|
||||
using var watchdog = StartWatchdog(options, host);
|
||||
heartbeat = StartHeartbeat(options, host);
|
||||
|
||||
await host.StartWebServerAsync();
|
||||
Log.Information("WebUI erreichbar unter {Url}", options.WebserverUrl);
|
||||
@@ -50,13 +57,14 @@ internal static class HeadlessRunner
|
||||
// Laeuft, bis abgebrochen wird.
|
||||
await host.StartWorkersAsync(cts.Token);
|
||||
|
||||
watchdog?.NotifyStopping();
|
||||
heartbeat?.NotifyStopping();
|
||||
await host.StopWebServerAsync();
|
||||
Log.Information("Dienst planmäßig beendet.");
|
||||
return ExitOk;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
heartbeat?.NotifyStopping();
|
||||
Log.Information("Dienst planmäßig beendet.");
|
||||
return ExitOk;
|
||||
}
|
||||
@@ -67,6 +75,7 @@ internal static class HeadlessRunner
|
||||
}
|
||||
finally
|
||||
{
|
||||
heartbeat?.Dispose();
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
}
|
||||
@@ -75,15 +84,12 @@ internal static class HeadlessRunner
|
||||
/// Lizenzpruefung ohne Dialog: zuerst der zwischengespeicherte Schluessel, sonst
|
||||
/// einmalige Aktivierung mit dem Schluessel aus der Umgebungsvariable.
|
||||
/// </summary>
|
||||
private static async Task<bool> EnsureLicensedAsync()
|
||||
private static async Task<LicenseSession?> EnsureLicensedAsync()
|
||||
{
|
||||
var result = await LicenseGuard.ValidateCachedAsync();
|
||||
if (result?.IsValid == true)
|
||||
{
|
||||
Log.Information("Lizenz gültig ({Status}{Cached}).",
|
||||
result.Status, result.IsCached ? ", aus lokalem Cache" : "");
|
||||
return true;
|
||||
}
|
||||
var client = LicenseGuard.CreateClient();
|
||||
|
||||
var check = await LicenseGuard.TryUseCachedAsync(client);
|
||||
if (check.Session is not null) return check.Session;
|
||||
|
||||
var key = Environment.GetEnvironmentVariable(LicenseKeyVariable);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
@@ -91,44 +97,76 @@ internal static class HeadlessRunner
|
||||
Log.Fatal("Keine nutzbare Lizenz ({Status}) und {Variable} ist nicht gesetzt. " +
|
||||
"Im Headless-Betrieb gibt es keinen Aktivierungsdialog — bitte den Lizenzschlüssel " +
|
||||
"über die Umgebungsvariable bereitstellen oder einmalig mit --license-set-key aktivieren.",
|
||||
result?.Status ?? "kein Schlüssel hinterlegt", LicenseKeyVariable);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.Information("Aktiviere Lizenz mit dem Schlüssel aus {Variable}…", LicenseKeyVariable);
|
||||
result = await LicenseGuard.ValidateAsync(key);
|
||||
if (result.IsValid)
|
||||
{
|
||||
Log.Information("Lizenz aktiviert ({Status}).", result.Status);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.Fatal("Aktivierung fehlgeschlagen ({Status}): {Message}", result.Status, result.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static WatchdogHeartbeatService? StartWatchdog(PredictalyticsOptions options, PredictalyticsHost host)
|
||||
{
|
||||
if (!options.WatchdogEnabled) return null;
|
||||
if (string.IsNullOrWhiteSpace(options.WatchdogApiKey) || string.IsNullOrWhiteSpace(options.WatchdogUrl))
|
||||
{
|
||||
Log.Information("🐕 Watchdog ist aktiviert, aber URL/API Key fehlen.");
|
||||
check.LastResult?.Status ?? "kein Schlüssel hinterlegt", LicenseKeyVariable);
|
||||
return null;
|
||||
}
|
||||
|
||||
var watchdog = new WatchdogHeartbeatService(
|
||||
options.WatchdogUrl,
|
||||
options.WatchdogApiKey,
|
||||
options.WatchdogSource,
|
||||
options.WatchdogInstance,
|
||||
options.WatchdogIntervalSeconds,
|
||||
metadataProvider: () => new
|
||||
Log.Information("Aktiviere Lizenz mit dem Schlüssel aus {Variable}…", LicenseKeyVariable);
|
||||
var (session, result) = await LicenseGuard.ActivateAsync(client, key);
|
||||
if (session is not null)
|
||||
{
|
||||
Log.Information("Lizenz aktiviert ({Status}).", result.Status);
|
||||
return session;
|
||||
}
|
||||
|
||||
Log.Fatal("Aktivierung fehlgeschlagen ({Status}): {Message}", result.Status, result.Message);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DcHeartbeatService? StartHeartbeat(PredictalyticsOptions options, PredictalyticsHost host)
|
||||
{
|
||||
if (!options.DcHeartbeatEnabled) return null;
|
||||
if (string.IsNullOrWhiteSpace(options.DcToken))
|
||||
{
|
||||
Log.Information("🐕 Heartbeat ist aktiviert, aber es fehlt das Deployment-Center-Token.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var heartbeat = new DcHeartbeatService(
|
||||
options.DcToken,
|
||||
options.DcSource,
|
||||
options.DcInstance,
|
||||
options.DcHeartbeatIntervalSeconds,
|
||||
ct => CollectSnapshotAsync(options, host, ct));
|
||||
heartbeat.Start();
|
||||
return heartbeat;
|
||||
}
|
||||
|
||||
private static async Task<DcHeartbeatSnapshot> CollectSnapshotAsync(
|
||||
PredictalyticsOptions options, PredictalyticsHost host, CancellationToken ct)
|
||||
{
|
||||
var snapshot = new DcHeartbeatSnapshot
|
||||
{
|
||||
Message = host.WorkersRunning ? "Worker laufen" : "Worker gestoppt"
|
||||
};
|
||||
|
||||
snapshot.Metrics["workers_running"] = host.WorkersRunning ? 1 : 0;
|
||||
snapshot.Metrics["webserver_running"] = host.WebServerRunning ? 1 : 0;
|
||||
snapshot.Metrics["memory_mb"] = Math.Round(GC.GetTotalMemory(forceFullCollection: false) / 1024d / 1024d, 1);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.DbName))
|
||||
{
|
||||
var started = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
workersRunning = host.WorkersRunning,
|
||||
webserverRunning = host.WebServerRunning
|
||||
});
|
||||
watchdog.Start();
|
||||
return watchdog;
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
await using var conn = new MySqlConnector.MySqlConnection(options.ConnectionString);
|
||||
await conn.OpenAsync(timeout.Token);
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT 1";
|
||||
await cmd.ExecuteScalarAsync(timeout.Token);
|
||||
|
||||
snapshot.Checks["db"] = new DcCheck(true, $"{started.ElapsedMilliseconds} ms", started.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
snapshot.Checks["db"] = new DcCheck(false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Deploymentcenter.Client;
|
||||
using Predictalytics.Hosting;
|
||||
|
||||
namespace Predictalytics.Shell.Services;
|
||||
@@ -18,26 +19,30 @@ internal static class LicenseCli
|
||||
|
||||
public static async Task<int> RunAsync(string[] args)
|
||||
{
|
||||
switch (args[0])
|
||||
LicenseClient.DefaultAppVersion = DcConfig.AppVersion;
|
||||
|
||||
return args[0] switch
|
||||
{
|
||||
case StatusSwitch: return await ShowStatusAsync();
|
||||
case SetKeySwitch: return await SetKeyAsync(args.Length > 1 ? args[1] : null);
|
||||
case DeactivateSwitch: return await DeactivateAsync();
|
||||
default: return 1;
|
||||
}
|
||||
StatusSwitch => await ShowStatusAsync(),
|
||||
SetKeySwitch => await SetKeyAsync(args.Length > 1 ? args[1] : null),
|
||||
DeactivateSwitch => await DeactivateAsync(),
|
||||
_ => HeadlessRunner.ExitFailed
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<int> ShowStatusAsync()
|
||||
{
|
||||
var hw = LicenseGuard.GetHardwareInfo();
|
||||
Console.WriteLine("Predictalytics — Lizenzstatus");
|
||||
Console.WriteLine($" Produkt : {LicenseGuard.ProductSlug}");
|
||||
Console.WriteLine($" Produkt : {DcConfig.ProductSlug}");
|
||||
Console.WriteLine($" Version : {DcConfig.AppVersion} ({DcConfig.GitCommitShort})");
|
||||
Console.WriteLine($" Server : {DcConfig.BaseUrl}");
|
||||
Console.WriteLine($" Hardware-ID : {hw.HardwareId}");
|
||||
Console.WriteLine($" HWID-Quelle : {hw.HwidSource} (v{hw.HwidVersion}, {hw.Platform})");
|
||||
Console.WriteLine($" Cache-Ablage : {LicenseGuard.StorageDirectory}");
|
||||
|
||||
var cachedKey = LicenseGuard.GetCachedLicenseKey();
|
||||
if (cachedKey == null)
|
||||
var cachedKey = LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
|
||||
if (string.IsNullOrWhiteSpace(cachedKey))
|
||||
{
|
||||
Console.WriteLine(" Schlüssel : keiner hinterlegt");
|
||||
Console.WriteLine();
|
||||
@@ -45,16 +50,28 @@ internal static class LicenseCli
|
||||
return HeadlessRunner.ExitNoLicense;
|
||||
}
|
||||
|
||||
Console.WriteLine($" Schlüssel : {Mask(cachedKey)}");
|
||||
Console.WriteLine($" Schlüssel : {Mask(cachedKey!)}");
|
||||
Console.WriteLine();
|
||||
Console.Write("Prüfe am Server… ");
|
||||
Console.Write("Prüfe am Deployment Center… ");
|
||||
|
||||
var result = await LicenseGuard.ValidateCachedAsync();
|
||||
Console.WriteLine(result is { IsValid: true }
|
||||
? $"gültig ({result.Status}{(result.IsCached ? ", aus lokalem Cache" : "")})"
|
||||
var check = await LicenseGuard.TryUseCachedAsync();
|
||||
var result = check.LastResult;
|
||||
|
||||
if (check.IsUsable)
|
||||
{
|
||||
Console.WriteLine($"gültig ({result!.Status}{(result.IsCached ? ", aus lokalem Cache" : "")})");
|
||||
if (result.CacheExpiresAt is { } expiresAt && expiresAt > 0)
|
||||
{
|
||||
Console.WriteLine($" Offline-Gnadenfrist bis: {DateTimeOffset.FromUnixTimeSeconds(expiresAt).UtcDateTime:yyyy-MM-dd HH:mm} UTC");
|
||||
}
|
||||
return HeadlessRunner.ExitOk;
|
||||
}
|
||||
|
||||
// Transient heisst: kein Urteil, nur eine gescheiterte Verbindung.
|
||||
Console.WriteLine(result?.IsTransient == true
|
||||
? $"unentschieden ({result.Status}): {result.Message}"
|
||||
: $"NICHT gültig ({result?.Status}): {result?.Message}");
|
||||
|
||||
return result is { IsValid: true } ? HeadlessRunner.ExitOk : HeadlessRunner.ExitNoLicense;
|
||||
return HeadlessRunner.ExitNoLicense;
|
||||
}
|
||||
|
||||
private static async Task<int> SetKeyAsync(string? key)
|
||||
@@ -65,10 +82,10 @@ internal static class LicenseCli
|
||||
return HeadlessRunner.ExitFailed;
|
||||
}
|
||||
|
||||
Console.Write($"Aktiviere {Mask(LicenseGuard.Normalize(key))} … ");
|
||||
var result = await LicenseGuard.ValidateAsync(key);
|
||||
Console.Write($"Aktiviere {Mask(key.Trim().ToUpperInvariant())} … ");
|
||||
var (session, result) = await LicenseGuard.ActivateAsync(LicenseGuard.CreateClient(), key);
|
||||
|
||||
if (result.IsValid)
|
||||
if (session is not null)
|
||||
{
|
||||
Console.WriteLine($"erfolgreich ({result.Status})");
|
||||
Console.WriteLine($"Hardware-ID: {result.HardwareId}");
|
||||
@@ -81,14 +98,16 @@ internal static class LicenseCli
|
||||
|
||||
private static async Task<int> DeactivateAsync()
|
||||
{
|
||||
if (LicenseGuard.GetCachedLicenseKey() == null)
|
||||
var key = LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
Console.Error.WriteLine("Kein hinterlegter Schlüssel — nichts zu deaktivieren.");
|
||||
return HeadlessRunner.ExitFailed;
|
||||
}
|
||||
|
||||
Console.Write("Gebe Aktivierungsplatz am Server frei… ");
|
||||
var ok = await LicenseGuard.DeactivateAsync();
|
||||
Console.Write("Gebe Aktivierungsplatz am Deployment Center frei… ");
|
||||
var ok = await LicenseGuard.CreateClient()
|
||||
.DeactivateAsync(DcConfig.ProductSlug, key!, DcConfig.BaseUrl);
|
||||
Console.WriteLine(ok ? "erfolgreich" : "fehlgeschlagen");
|
||||
return ok ? HeadlessRunner.ExitOk : HeadlessRunner.ExitFailed;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user