Watchdog-Heartbeat + LicenseLabrador-Lizenzpruefung eingebunden

Slices W-A/W-B/W-C und L-A aus UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.

Watchdog (Dead-Man's-Switch, externer Server):
- WatchdogHeartbeatService als BackgroundService + DI-Singleton; jeder Sendeversuch
  gekapselt, ein Ausfall des Watchdogs beeintraechtigt PolyTrader nie.
- Eigene Implementierung statt Test-Client des Fremdprojekts: TLS-Pruefung bleibt
  aktiv, http:// nur fuer localhost (Agent-Token nicht im Klartext ins Netz).
- Status aus dem App-Log abgeleitet (Error mit 5-Minuten-Sticky-Fenster, entprellt),
  Lifecycle-Events started/stopping.
- Konfiguration in ServerSettings; Agent-Token [Browsable(false)] mit maskierter
  Eingabe + Statusanzeige, bei gesetztem Master-Key verschluesselt (enc:v1:).

Lizenz (LicenseLabrador, Ed25519):
- LicenseGate.RunStartupGate prueft beim Start; bei ungueltiger Lizenz wird die
  Modulliste leer gebaut, sodass nur die Core-Shell (Terminal/Einstellungen)
  startet. Bewusst kein Environment.Exit - ein Trading-Bot darf nicht mitten im
  Lauf hart sterben. TamperSuspected gilt als nicht nutzbar.
- LicenseDialog (partial + .Designer.cs) fuer Start- und Verwalten-Modus, mit
  Hardware-ID zum Kopieren; Smoke-UI konstruiert beide Modi headless.
- Master-Key wird jetzt VOR dem Host-Build geladen, da auch der Lizenzschluessel
  entschluesselt werden muss; derselbe TerminalLogger wird als Singleton
  weitergereicht, damit die Startmeldungen im Terminal-Fenster erscheinen.

Der Lizenz-SDK-Client wird per Cross-Repo-Projektreferenz auf
..\..\LicenseLabrador eingebunden, damit SDK-Fixes ohne Kopie einfliessen.

484 Zeilen Tests fuer den Heartbeat; Suite gruen (438 Tests).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-04 09:04:14 +02:00
co-authored by Claude Opus 5
parent bb103a578a
commit ca750a06f5
11 changed files with 2122 additions and 8 deletions
+41 -6
View File
@@ -69,6 +69,14 @@ internal static class Program
// erst geladen werden. Dieselbe Instanz wird als Singleton weitergereicht (eine Quelle).
var serverSettings = PolyTraderSharp.Models.ServerSettings.Load(ServerSettingsPath);
// Früher Logger: wird unten als DI-Singleton weitergereicht, damit die Startmeldungen
// (Master-Key, Lizenz) später im Terminal-Fenster erscheinen (History-Replay).
var bootLog = new TerminalLogger();
// F1 (Sicherheit): Master-Key VOR jeder Entschlüsselung laden wird sowohl für die
// Account-Credentials als auch für den gespeicherten Lizenzschlüssel gebraucht.
ConfigureSecretProtection(bootLog);
// Alle bekannten Module; „modules" enthält nur die aktiven (nicht in DisabledModules).
var allModules = new System.Collections.Generic.List<IPolyTraderModule>
{
@@ -79,7 +87,15 @@ internal static class Program
};
var disabledModules = new System.Collections.Generic.HashSet<string>(
serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);
var modules = allModules.Where(m => !disabledModules.Contains(m.Name)).ToList();
// B.4: Lizenzprüfung beim Start. Bei ungültiger Lizenz zeigt der Gate einen Dialog und
// gibt false zurück → es werden KEINE Module registriert (nur die Core-Shell startet,
// damit Terminal und Einstellungen zum Eintragen einer Lizenz erreichbar bleiben).
// Bewusst KEIN Environment.Exit ein Trading-Bot darf nicht mitten im Lauf hart sterben.
bool licensed = PolyTraderSharp.Licensing.LicenseGate.RunStartupGate(serverSettings, bootLog, ServerSettingsPath);
var modules = licensed
? allModules.Where(m => !disabledModules.Contains(m.Name)).ToList()
: new System.Collections.Generic.List<IPolyTraderModule>();
AppHost = Host.CreateDefaultBuilder()
// Config immer neben der EXE suchen (nicht im Arbeitsverzeichnis), damit die App
@@ -126,11 +142,17 @@ internal static class Program
}
});
});
services.AddSingleton<TerminalLogger>();
// Denselben früh erzeugten Logger als Singleton weiterreichen, damit die
// Master-Key-/Lizenz-Meldungen von oben im Terminal-Fenster auftauchen.
services.AddSingleton(bootLog);
services.AddSingleton<MullvadVpnService>();
services.AddSingleton<ThreemaService>();
services.AddSingleton<JobManager>();
// Watchdog-Heartbeat (Dead-Man's-Switch). Als Singleton registriert, damit das
// Settings-Fenster denselben Dienst für ReloadSettings/Test-Heartbeat nutzt.
services.AddSingleton(sp => new WatchdogHeartbeatService(sp.GetRequiredService<TerminalLogger>()));
// MUSS als erster HostedService laufen: hydriert den State, bevor die
// Trading-Services gegen einen leeren State anlaufen.
services.AddHostedService<StartupHydrationService>();
@@ -145,13 +167,12 @@ internal static class Program
services.AddHostedService<MarketSyncService>();
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<MullvadVpnService>());
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<ThreemaService>());
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<WatchdogHeartbeatService>());
services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
}).Build();
// F1 (Sicherheit): Master-Key VOR jeder Credential-Entschlüsselung (Hydration) laden.
// Quelle: Umgebungsvariable POLYTRADER_MASTER_KEY, sonst gitignorierte master.key im App-Ordner.
ConfigureSecretProtection(AppHost.Services.GetRequiredService<TerminalLogger>());
// (Master-Key wurde bereits VOR dem Host-Build geladen siehe ConfigureSecretProtection(bootLog) oben.)
// F5 (Sicherheit): warnen, wenn die (remote) DB-Verbindung keine TLS-Erzwingung hat.
WarnIfDbTlsNotEnforced(AppHost.Services);
@@ -212,7 +233,8 @@ internal static class Program
viewServices.GetRequiredService<MullvadVpnService>(),
viewServices.GetRequiredService<TerminalLogger>(),
viewServices.GetRequiredService<PolyTrader.Core.Persistence.IAccountRepository>(),
viewServices.GetRequiredService<TradingState>());
viewServices.GetRequiredService<TradingState>(),
viewServices.GetRequiredService<WatchdogHeartbeatService>());
return view;
}
});
@@ -533,6 +555,19 @@ internal static class Program
catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] {id}: {ex.GetType().Name}: {ex.Message}"); }
}
// B.4: Lizenzdialog headless konstruieren (kein Netz, keine Message-Loop) fängt
// Designer-/Konstruktionsregressionen ab.
try
{
var licClient = new LicenseLabrador.Client.LicenseClient(PolyTraderSharp.Licensing.LicenseGate.BuildConfig());
var dummy = new LicenseLabrador.Client.LicenseResult(
LicenseLabrador.Client.LicenseState.NoLicense, null, null, "Smoke-Test", string.Empty);
using (var licDlg = new PolyTraderSharp.Ui.LicenseDialog(licClient, string.Empty, dummy, startupContext: true))
using (var licDlgManage = new PolyTraderSharp.Ui.LicenseDialog(licClient, string.Empty, null, startupContext: false))
Console.WriteLine("[OK] license.dialog konstruiert (Start- und Verwalten-Modus)");
}
catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] license.dialog: {ex.GetType().Name}: {ex.Message}"); }
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
return failures;
}