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>
153 lines
6.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|