Files
PolyTraderSharp/Ui/LicenseDialog.cs
T
RichardandClaude Opus 5 ca750a06f5 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>
2026-08-04 09:04:14 +02:00

130 lines
5.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Drawing;
using System.Windows.Forms;
using LicenseLabrador.Client;
using PolyTraderSharp.Licensing;
namespace PolyTraderSharp.Ui
{
/// <summary>
/// Modaler Lizenzdialog beim Programmstart, wenn keine gültige Lizenz vorliegt. Zeigt die
/// Hardware-ID (kopierbar, für die Aktivierung im Lizenz-Admin), nimmt einen Lizenzschlüssel
/// entgegen und validiert ihn gegen den Lizenzserver. Bei Erfolg werden <see cref="ValidatedKey"/>
/// und <see cref="ValidatedResult"/> gesetzt; der Aufrufer (LicenseGate) speichert den Schlüssel.
/// Ohne gültige Lizenz kann der Nutzer eingeschränkt (nur Core-Shell) weiterstarten.
/// </summary>
public partial class LicenseDialog : Form
{
private readonly LicenseClient _client;
/// <summary>Nur gesetzt, wenn im Dialog erfolgreich validiert wurde.</summary>
public LicenseResult? ValidatedResult { get; private set; }
/// <summary>Der erfolgreich validierte Schlüssel (für die verschlüsselte Ablage).</summary>
public string? ValidatedKey { get; private set; }
/// <param name="startupContext">
/// true = Aufruf beim Programmstart (keine gültige Lizenz): der Abbruch-Button bietet
/// „eingeschränkt starten". false = Aufruf aus den Einstellungen (App läuft bereits): der
/// Button heißt „Schließen".
/// </param>
public LicenseDialog(LicenseClient client, string prefillKey, LicenseResult? initialResult, bool startupContext = true)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
InitializeComponent();
txtHardwareId.Text = SafeHardwareId(client);
txtKey.Text = prefillKey ?? string.Empty;
if (initialResult != null)
{
lblStatus.Text = "Status: " + LicenseGate.Describe(initialResult) +
(string.IsNullOrEmpty(initialResult.Message) ? string.Empty : $" {initialResult.Message}");
}
else
{
lblStatus.Text = "Status: noch nicht geprüft bitte „Validieren“ klicken.";
}
if (startupContext)
{
lblHeadline.Text = "Für PolyTrader wird eine gültige Lizenz benötigt.";
btnLimited.Text = "Ohne Lizenz starten (eingeschränkt)";
}
else
{
lblHeadline.Text = "Lizenz prüfen / hinterlegen";
btnLimited.Text = "Schließen";
}
btnValidate.Click += (_, _) => Validate();
btnCopyHwId.Click += (_, _) => CopyHardwareId();
btnLimited.Click += (_, _) => { DialogResult = DialogResult.Cancel; Close(); };
AcceptButton = btnValidate;
}
private static string SafeHardwareId(LicenseClient client)
{
try { return client.GetHardwareId(); }
catch { return "(nicht ermittelbar)"; }
}
private void Validate()
{
string key = txtKey.Text.Trim();
if (string.IsNullOrWhiteSpace(key))
{
lblResult.ForeColor = Color.Firebrick;
lblResult.Text = "Bitte einen Lizenzschlüssel eingeben.";
return;
}
btnValidate.Enabled = false;
Cursor = Cursors.WaitCursor;
lblResult.ForeColor = SystemColors.ControlText;
lblResult.Text = "Prüfe Lizenz …";
lblResult.Refresh();
try
{
// Blockierend: das SDK nutzt durchgängig ConfigureAwait(false) → kein Deadlock auf dem
// UI-Thread; der kurze Freeze (max. HTTP-Timeout) ist im modalen Startdialog vertretbar
// und vermeidet jede Abhängigkeit vom WinForms-SynchronizationContext vor Application.Run.
LicenseResult result = _client.ValidateAsync(key).GetAwaiter().GetResult();
if (result.IsUsable)
{
ValidatedResult = result;
ValidatedKey = key;
lblResult.ForeColor = Color.Green;
lblResult.Text = "Lizenz gültig PolyTrader startet.";
DialogResult = DialogResult.OK;
Close();
}
else
{
lblResult.ForeColor = Color.Firebrick;
string extra = string.IsNullOrEmpty(result.Message) ? string.Empty : $" ({result.Message})";
lblResult.Text = "Nicht gültig: " + LicenseGate.Describe(result) + extra;
}
}
catch (Exception ex)
{
lblResult.ForeColor = Color.Firebrick;
lblResult.Text = "Fehler bei der Prüfung: " + ex.Message;
}
finally
{
Cursor = Cursors.Default;
btnValidate.Enabled = true;
}
}
private void CopyHardwareId()
{
try { Clipboard.SetText(txtHardwareId.Text); }
catch { /* Zwischenablage kann kurzzeitig belegt sein kein harter Fehler */ }
}
}
}