Files
PolyTraderSharp/src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs
T
RichardandClaude Opus 5 9b2294cb4c
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s
Testanleitung: eigenes X-Display je Projekt fuer Parallelbetrieb
Damit mehrere Agenten gleichzeitig Oberflaechen testen koennen, bekommt jedes
Projekt eine feste Displaynummer (Vorgabe Richard):

  :99 PolyTrader, :98 Predictalytics, :97 ClawdDotNet, :96 IBKRTrader,
  :95 und absteigend fuer kuenftige Projekte.

Zwei Prozesse auf demselben Display sehen die Fenster des jeweils anderen -
Klicks landen im falschen Programm, Aufnahmen zeigen fremde Anwendungen.

Beim Durcharbeiten fielen zwei weitere Kollisionspunkte auf, die zum
Parallelbetrieb dazugehoeren und mit aufgenommen wurden:

- Screenshot- und Logdateien in /tmp brauchen ein Projektpraefix. /tmp/v_1.png
  ueberschreiben sich parallel laufende Projekte gegenseitig.
- "xvfb-run -a" darf NICHT mehr verwendet werden: Das -a sucht sich selbst ein
  freies Display und beginnt dabei bei :99 - ausgerechnet PolyTraders Nummer.
  Stattdessen "xvfb-run -n <nummer>".

Die Warnung ist nicht theoretisch: Auf der VM lag ein verwaister X98-Socket,
entstanden durch genau so einen "xvfb-run -a"-Aufruf beim smoke-ui-Test, als
:99 belegt war. Er blockierte Predictalytics' reservierte Nummer, ohne dass ein
Prozess lief. Entfernt, und die Anleitung erklaert jetzt, wie man verwaiste
Sockets erkennt (ls /tmp/.X11-unix/ gegen pgrep -a Xvfb lesen) und aufraeumt.

Weiter ergaenzt:
- pkill nie ohne Displaynummer - "pkill Xvfb" erschlaegt die Sitzungen aller
  anderen Projekte.
- DISPLAY in jedem Skript setzen, sonst landet man auf :0, dem echten Desktop.
- Die Skriptvorlage hat jetzt einen Kopf mit PROJEKT/DISP/FENSTER; das ist
  alles, was sich je Projekt unterscheidet. Praktisch gegengetestet.
- Aufraeumabschnitt und Belegungsliste um Displays und Verzeichnisse erweitert.

Nebenbei aufgefallen: Das PolyTrader-Publish lag unter ~/pt-test statt
~/polytrader-test und verletzte damit die eigene Verzeichniskonvention.
Umbenannt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 20:49:28 +02:00

153 lines
6.5 KiB
C#

using System;
using System.Linq;
using Deploymentcenter.Client;
using PolyTrader.Core;
using PolyTrader.Core.Security;
using PolyTraderSharp.Models;
namespace PolyTrader.App.Avalonia.Licensing
{
/// <summary>
/// Kopflose Lizenzverwaltung von der Kommandozeile aus (D-2), nach der Konvention aus
/// LICENSE_INTEGRATION_GUIDE §4: <c>--license-status</c>, <c>--license-set-key</c>,
/// <c>--license-deactivate</c>. Diese Schalter baut die Anwendung selbst, das SDK bringt nur die
/// Bausteine mit. Läuft VOR dem Host-Aufbau und beendet den Prozess über den Rückgabewert.
/// </summary>
internal static class LicenseCli
{
/// <summary>Liefert <c>true</c>, wenn einer der Lizenz-Schalter erkannt und abgearbeitet wurde.</summary>
public static bool TryHandle(string[] args, out int exitCode)
{
exitCode = 0;
if (args.Any(a => string.Equals(a, "--license-status", StringComparison.OrdinalIgnoreCase)))
{
exitCode = Status();
return true;
}
int setIdx = Array.FindIndex(args, a => string.Equals(a, "--license-set-key", StringComparison.OrdinalIgnoreCase));
if (setIdx >= 0)
{
string? key = setIdx + 1 < args.Length ? args[setIdx + 1] : null;
exitCode = SetKey(key);
return true;
}
if (args.Any(a => string.Equals(a, "--license-deactivate", StringComparison.OrdinalIgnoreCase)))
{
exitCode = Deactivate();
return true;
}
return false;
}
private static int Status()
{
var options = Program.LoadDeploymentcenterOptions();
var hw = HardwareId.GetHardwareId(options.ProjectSlug);
var cached = LicenseClient.TryGetCachedState(options.ProjectSlug);
Console.WriteLine($"HWID v2: {hw.HardwareId} ({hw.HwidSource})");
Console.WriteLine($"Produkt: {options.ProjectSlug}");
Console.WriteLine($"Server: {(string.IsNullOrWhiteSpace(options.BaseUrl) ? "(keine URL konfiguriert)" : options.BaseUrl)}");
Console.WriteLine($"Cache: {(cached == null ? "leer" : cached.Status)}");
if (cached != null && cached.CacheExpiresAt > 0)
{
var until = DateTimeOffset.FromUnixTimeSeconds(cached.CacheExpiresAt);
Console.WriteLine($"Offline noch bis: {until:u}");
}
var settings = ServerSettings.Load("server_settings.xml");
Console.WriteLine($"Gespeicherter Schlüssel (server_settings.xml): {(string.IsNullOrWhiteSpace(settings.LicenseKey) ? "keiner" : settings.LicenseKeyStatus)}");
return 0;
}
private static int SetKey(string? key)
{
if (string.IsNullOrWhiteSpace(key))
{
Console.Error.WriteLine("Fehler: --license-set-key erwartet den Schlüssel als nächstes Argument.");
return 1;
}
var options = Program.LoadDeploymentcenterOptions();
if (string.IsNullOrWhiteSpace(options.BaseUrl))
{
Console.Error.WriteLine("Fehler: Keine Deploymentcenter-URL konfiguriert (appsettings.json: Deploymentcenter:BaseUrl).");
return 1;
}
LicenseClient.DefaultAppVersion = BuildInfo.Version;
var client = new LicenseClient();
LicenseValidationResult result = client
.ValidateAsync(options.ProjectSlug, key.Trim(), options.BaseUrl, BuildInfo.Version)
.GetAwaiter().GetResult();
Console.WriteLine($"{result.Status}: {result.Message}");
if (!result.IsValid)
{
return 1;
}
const string settingsPath = "server_settings.xml";
var settings = ServerSettings.Load(settingsPath);
// Master-Key laden, BEVOR der Schlüssel abgelegt wird. Ohne diesen Aufruf reicht
// SecretProtection.Protect den Klartext unveraendert durch - genau das war der Grund,
// warum --license-status "gesetzt (Klartext)" meldete (Befund B-12).
Program.ConfigureSecretProtection(Console.Error.WriteLine);
settings.LicenseKey = SecretProtection.Protect(key.Trim());
settings.Save(settingsPath);
Console.WriteLine($"Lizenz gültig und in {settingsPath} gespeichert " +
$"({(SecretProtection.IsEncrypted(settings.LicenseKey) ? "verschlüsselt" : "Klartext")}).");
return 0;
}
private static int Deactivate()
{
var options = Program.LoadDeploymentcenterOptions();
var settings = ServerSettings.Load("server_settings.xml");
// Ein verschluesselt abgelegter Schluessel (B-12) ist ohne Master-Key nicht lesbar.
Program.ConfigureSecretProtection(Console.Error.WriteLine);
string key;
try { key = SecretProtection.Unprotect(settings.LicenseKey); }
catch (Exception ex)
{
Console.Error.WriteLine($"Fehler: gespeicherter Schlüssel nicht lesbar: {ex.Message}");
return 1;
}
if (string.IsNullOrWhiteSpace(key))
{
Console.Error.WriteLine("Fehler: kein Lizenzschlüssel hinterlegt.");
return 1;
}
// Die Deaktivierung gibt einen Aktivierungsplatz frei und ist deshalb geschützt - der
// shared_key gehört auf den Administrationsrechner, nicht in die ausgelieferte
// Anwendung (siehe LICENSE_INTEGRATION_GUIDE). Ohne ihn: klarer Hinweis statt Versuch.
string? sharedKey = Environment.GetEnvironmentVariable("DC_SHARED_KEY");
if (string.IsNullOrWhiteSpace(sharedKey))
{
Console.Error.WriteLine("Fehler: DC_SHARED_KEY nicht gesetzt. Die Deaktivierung braucht den " +
"administrativen Shared Key - alternativ im Deploymentcenter-WebUI unter " +
"der Hardware-Liste auf „Freigeben\" klicken.");
return 1;
}
var client = new LicenseClient();
bool ok = client.DeactivateAsync(options.ProjectSlug, key, options.BaseUrl, sharedKey)
.GetAwaiter().GetResult();
Console.WriteLine(ok ? "Aktivierung freigegeben." : "Deaktivierung fehlgeschlagen.");
return ok ? 0 : 1;
}
}
}