Lizenzsystem auf Deploymentcenter umgestellt
Predictalytics hing noch am abgeloesten LicenseLabrador (license.mhdf.de), waehrend die Lizenzen inzwischen im Deploymentcenter (dc.mhdf.de) gepflegt werden. Vor dem Umbau mit einem Wegwerf-Probelauf gegen den echten Client abgesichert: Slug predictalytics, HWID v2 aus machine-guid, Server meldet valid. Hosting/LicenseGuard.cs auf Deploymentcenter.Client umgeschrieben. Die Aufteilung bleibt: plattformneutrale Pruefung im Hosting, interaktive Aktivierung in den Hosts. Gewinn fuer die Linux-Portierung: - Hardware-ID v2 mit Quellen-Kaskade /etc/machine-id, dbus, DMI-UUID, MAC, Schluesseldatei; Container ueber DEPLOYMENTCENTER_HWID - Der Rechnername steckt nicht mehr im Hash, Umbenennen verbraucht keinen Aktivierungsplatz - Cache mit AES-256-GCM statt DPAPI, ohne stillen Klartext-Rueckfall unter Linux. Damit erledigt sich Punkt 3.2 Nr. 2 der Portierungsanalyse. - XDG_CONFIG_HOME wird beruecksichtigt - Keine Basic-Auth-Zugangsdaten mehr im Quelltext Neue CLI-Schalter aus dem Integrationsleitfaden, ausgewertet vor jeder Avalonia-Initialisierung: --license-status, --license-set-key, --license-deactivate. Schluessel werden in der Ausgabe maskiert. Die Lizenzfenster beider Hosts zeigen jetzt die Hardware-ID an, an die der Server die Aktivierung bindet. Build: 0 Fehler. Tests: 100 bestanden. Verifiziert: --license-status meldet gueltig; die GUI startet vollstaendig durch bis MainWindowViewModel.Start(), belegt durch die Logdateien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -309,12 +309,22 @@ Beide Betriebsarten laufen an:
|
|||||||
| `Predictalytics.Shell.exe` (GUI) | startet, Lizenzfenster erscheint, kein Absturz |
|
| `Predictalytics.Shell.exe` (GUI) | startet, Lizenzfenster erscheint, kein Absturz |
|
||||||
| Konsolen-Kodierung | UTF-8 explizit gesetzt — ohne das zeigt die Windows-Konsole nur Fragezeichen statt Rahmen und Emoji |
|
| Konsolen-Kodierung | UTF-8 explizit gesetzt — ohne das zeigt die Windows-Konsole nur Fragezeichen statt Rahmen und Emoji |
|
||||||
|
|
||||||
### ⚠️ Nicht verifiziert: das Hauptfenster
|
### Nachtrag 2026-08-07: Hauptfenster verifiziert ✅
|
||||||
|
|
||||||
Die hinterlegte Lizenz ist **abgelaufen** (`Expired: License has expired`). Beide
|
Zunächst blockierte eine abgelaufene Lizenz die Prüfung. Nach der Umstellung auf das
|
||||||
Betriebsarten bleiben deshalb an der Lizenzschranke stehen — `MainWindow`, Terminal,
|
Deploymentcenter (Abschnitt 5a) startet die GUI vollständig durch — belegt durch die
|
||||||
Einstellungsansicht und die Wartungsaktionen sind compilergeprüft, aber **noch nie
|
Logdateien des Laufs:
|
||||||
angezeigt worden**. Nach Erneuerung der Lizenz nachzuholen.
|
|
||||||
|
```
|
||||||
|
[INF] Shell initialisiert. Bereit.
|
||||||
|
[INF] 'Start Webserver' startet die WebUI auf http://localhost:5000
|
||||||
|
[INF] Einstellungen: C:\Users\bergm\AppData\Roaming\Predictalytics\settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Damit sind Lizenzschranke, Serilog-Aufbau mit Terminal-Sink, `MainWindowViewModel.Start()`
|
||||||
|
und der neue Einstellungspfad nachgewiesen. **Noch nicht durchgeklickt**: die einzelnen
|
||||||
|
Wartungsaktionen und die Einstellungsansicht — dafür fehlen die in Phase 1 verlorenen
|
||||||
|
DB-Zugangsdaten.
|
||||||
|
|
||||||
### Umsetzung
|
### Umsetzung
|
||||||
|
|
||||||
@@ -433,6 +443,55 @@ Lizenzdialog.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 5a. Zusatz — Umstellung auf das Deploymentcenter-Lizenzsystem ✅ (2026-08-07)
|
||||||
|
|
||||||
|
**Nicht ursprünglich geplant.** Beim Aktivieren der neuen Lizenz stellte sich heraus, dass
|
||||||
|
Predictalytics noch am alten Lizenzsystem hing (LicenseLabrador, `license.mhdf.de`),
|
||||||
|
während die Lizenzen inzwischen im Deploymentcenter (`dc.mhdf.de`) gepflegt werden.
|
||||||
|
|
||||||
|
Vor dem Umbau mit einem Wegwerf-Probelauf gegen den echten Client abgesichert:
|
||||||
|
Slug `predictalytics`, HWID v2 aus `machine-guid`, Server meldet `valid`.
|
||||||
|
|
||||||
|
### Was sich geändert hat
|
||||||
|
|
||||||
|
| | vorher (LicenseLabrador) | jetzt (Deploymentcenter) |
|
||||||
|
|---|---|---|
|
||||||
|
| Endpunkt | `license.mhdf.de/public/api/v1` | `dc.mhdf.de/api/license/v1` |
|
||||||
|
| Client | `LicenseLabrador.Client` (netstandard2.0) | `Deploymentcenter.Client` (netstandard2.0;net8.0) |
|
||||||
|
| Authentifizierung | Ed25519-Signaturprüfung + Basic Auth im Quelltext | serverseitig, kein Geheimnis im Client |
|
||||||
|
| Hardware-ID | SHA-256 aus MachineGuid **+ Rechnername** | **v2**: `2:<plattform>:<hex>`, ohne Rechnername |
|
||||||
|
| Linux-HWID | `/etc/machine-id`, sonst MAC | `/etc/machine-id` → dbus → DMI-UUID → MAC → Schlüsseldatei |
|
||||||
|
| Container | nicht vorgesehen | `DEPLOYMENTCENTER_HWID` überschreibt |
|
||||||
|
| Cache | DPAPI, **stiller Klartext-Rückfall unter Linux** | AES-256-GCM, HKDF aus HWID+Slug, **kein Klartext-Rückfall**, `0600` unter Linux |
|
||||||
|
| Ablageort | `%APPDATA%` | zusätzlich `XDG_CONFIG_HOME` unter Linux |
|
||||||
|
|
||||||
|
**Damit erledigt sich Analyse-Punkt 3.2 Nr. 2 von selbst:** der stille Klartext-Rückfall
|
||||||
|
des Lizenz-Caches unter Linux existiert im neuen Client nicht mehr. Der Rechnername steckt
|
||||||
|
außerdem nicht mehr im Hash — Umbenennen verbraucht keinen Aktivierungsplatz.
|
||||||
|
|
||||||
|
### Neue CLI-Schalter (`Services/LicenseCli.cs`)
|
||||||
|
|
||||||
|
Aus dem Deploymentcenter-Integrationsleitfaden übernommen, für Server ohne Oberfläche:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Predictalytics.Shell --license-status # HWID, Cache-Ablage, Serverprüfung
|
||||||
|
Predictalytics.Shell --license-set-key <SCHLÜSSEL> # einmalig aktivieren
|
||||||
|
Predictalytics.Shell --license-deactivate # Aktivierungsplatz freigeben
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Schalter werden **vor** jeder Avalonia-Initialisierung ausgewertet, wie `--headless`.
|
||||||
|
Der Schlüssel wird in der Ausgabe maskiert (`LLAB3-*****-*****-*****-AABB2`).
|
||||||
|
|
||||||
|
### ⚠️ Offen: Watchdog läuft noch auf dem alten System
|
||||||
|
|
||||||
|
Das Deploymentcenter bündelt License, Watchdog, UpdateService und Bugtracker.
|
||||||
|
`WatchdogHeartbeatService` zeigt weiterhin auf `watchdog.mhdf.de` mit dem Header
|
||||||
|
`X-Watchdog-Key`. Das funktioniert, ist aber dieselbe Altlast wie eben beim Lizenzsystem.
|
||||||
|
Migration nach `dc.mhdf.de/api/watchdog/v1` als eigener Schritt einplanen —
|
||||||
|
Leitfaden liegt unter `Deploymentcenter/docs/WATCHDOG_INTEGRATION_GUIDE.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. Phase 4 — Aufräumen und Absichern (1–1,5 PT)
|
## 6. Phase 4 — Aufräumen und Absichern (1–1,5 PT)
|
||||||
|
|
||||||
### AP 4.1 — Kestrel-Binding und Absicherung ⚠️
|
### AP 4.1 — Kestrel-Binding und Absicherung ⚠️
|
||||||
|
|||||||
@@ -1,71 +1,96 @@
|
|||||||
using LicenseLabrador.Client;
|
using Deploymentcenter.Client;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Predictalytics.Hosting;
|
namespace Predictalytics.Hosting;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lizenzpruefung gegen den LicenseLabrador-Server.
|
/// Lizenzpruefung gegen das Deploymentcenter (dc.mhdf.de).
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Endpunkt, Produkt-Slug und der Ed25519-Public-Key sind bewusst einkompiliert
|
/// Endpunkt und Produkt-Slug sind bewusst einkompiliert (keine Benutzer-
|
||||||
/// (keine Benutzerkonfiguration): ein konfigurierbarer Endpunkt bzw. Key wuerde es
|
/// konfiguration): ein konfigurierbarer Endpunkt wuerde es erlauben, die
|
||||||
/// erlauben, die Anwendung auf einen gefaelschten Lizenzserver zu zeigen.
|
/// Anwendung auf einen gefaelschten Lizenzserver zu zeigen.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Enthaelt keinen Dialog: die interaktive Aktivierung liegt beim Host (WinForms-
|
/// Enthaelt keinen Dialog: die interaktive Aktivierung liegt beim Host
|
||||||
/// bzw. Avalonia-Dialog), der Headless-Modus bezieht den Schluessel aus der
|
/// (Avalonia- bzw. WinForms-Fenster), der Headless-Modus bezieht den
|
||||||
/// Konfiguration.
|
/// Schluessel aus der Umgebungsvariable oder dem verschluesselten Cache.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class LicenseGuard
|
public static class LicenseGuard
|
||||||
{
|
{
|
||||||
private const string ProductSlug = "predictalytics";
|
/// <summary>In dc_projects hinterlegter Slug.</summary>
|
||||||
private const string PublicKeyBase64 = "L7YR1wMKk8+lNefatzL+DMvAtHFVkZWYXAxXGrro+/U=";
|
public const string ProductSlug = "predictalytics";
|
||||||
private const string BasicAuthUser = "Labrador";
|
|
||||||
private const string BasicAuthPassword = "Labrador02763!";
|
private const string ServerUrl = "https://dc.mhdf.de";
|
||||||
// HTTPS ist Pflicht, nicht Kosmetik: license.mhdf.de leitet http→https um, und .NET
|
|
||||||
// macht bei einem Redirect aus dem POST ein GET. Der Server antwortet darauf mit 405,
|
|
||||||
// das SDK wertet das als "unerreichbar" und meldet irrefuehrend NoLicense.
|
|
||||||
// Ausserdem gingen die BasicAuth-Credentials sonst im Klartext ueber die Leitung.
|
|
||||||
private static readonly string[] Endpoints = { "https://license.mhdf.de/public/api/v1" };
|
|
||||||
|
|
||||||
/// <summary>Re-check interval while the app is running (12 h).</summary>
|
/// <summary>Re-check interval while the app is running (12 h).</summary>
|
||||||
public static readonly TimeSpan RevalidationInterval = TimeSpan.FromHours(12);
|
public static readonly TimeSpan RevalidationInterval = TimeSpan.FromHours(12);
|
||||||
|
|
||||||
public static LicenseClient CreateClient()
|
private static readonly LicenseClient Client = new();
|
||||||
|
|
||||||
|
/// <summary>Hardware-ID v2 dieser Maschine (Format <c>2:<plattform>:<hex></c>).</summary>
|
||||||
|
public static HardwareIdResult GetHardwareInfo() => HardwareId.GetHardwareId(ProductSlug);
|
||||||
|
|
||||||
|
/// <summary>Ablageort des verschluesselten Lizenz-Caches.</summary>
|
||||||
|
public static string StorageDirectory => LicenseConfig.GetStorageDirectory(ProductSlug);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zwischengespeicherter Lizenzschluessel, oder null wenn noch nie aktiviert wurde.
|
||||||
|
/// Der Cache ist AES-GCM-verschluesselt und an Hardware-ID und Produkt gebunden.
|
||||||
|
/// </summary>
|
||||||
|
public static string? GetCachedLicenseKey()
|
||||||
{
|
{
|
||||||
var config = new LicenseConfig
|
try
|
||||||
{
|
{
|
||||||
ProductSlug = ProductSlug,
|
var cache = StateStore.Load(ProductSlug, GetHardwareInfo().HardwareId);
|
||||||
PublicKeyBase64 = PublicKeyBase64,
|
return string.IsNullOrWhiteSpace(cache?.LicenseKey) ? null : cache!.LicenseKey;
|
||||||
Endpoints = Endpoints,
|
}
|
||||||
HttpBasicAuthUser = BasicAuthUser,
|
catch (Exception ex)
|
||||||
HttpBasicAuthPassword = BasicAuthPassword,
|
{
|
||||||
OfflineGraceHoursFallback = 168 // 7 Tage offline nutzbar, danach Serverkontakt nötig
|
Log.Debug(ex, "Lizenz-Cache konnte nicht gelesen werden");
|
||||||
};
|
return null;
|
||||||
return new LicenseClient(config);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prueft die zwischengespeicherte Lizenz. Liefert das Ergebnis; ob es nutzbar ist,
|
/// Validiert einen Lizenzschluessel am Server und legt ihn bei Erfolg
|
||||||
/// beantwortet <see cref="IsUsable"/>.
|
/// verschluesselt ab. Bei Netzwerkfehlern faellt der Client auf den
|
||||||
|
/// Cache zurueck (Status <c>valid_offline</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Task<LicenseResult> RevalidateAsync(LicenseClient client) => client.RevalidateAsync();
|
public static Task<LicenseValidationResult> ValidateAsync(string licenseKey) =>
|
||||||
|
Client.ValidateAsync(ProductSlug, Normalize(licenseKey), ServerUrl);
|
||||||
/// <summary>Nutzbar heisst: gueltig <em>und</em> mit passender Pruefsumme.</summary>
|
|
||||||
public static bool IsUsable(LicenseClient client, LicenseResult? result) =>
|
|
||||||
result != null && result.IsUsable && client.VerifyChecksum(result);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Startet die periodische Revalidierung im Hintergrund. Erkennt Widerruf/Ablauf
|
/// Validiert mit dem zwischengespeicherten Schluessel.
|
||||||
/// waehrend des Betriebs.
|
/// Liefert null, wenn noch keiner hinterlegt ist.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<LicenseValidationResult?> ValidateCachedAsync()
|
||||||
|
{
|
||||||
|
var key = GetCachedLicenseKey();
|
||||||
|
return key == null ? null : await ValidateAsync(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gibt den Aktivierungsplatz dieser Maschine am Server wieder frei.</summary>
|
||||||
|
public static async Task<bool> DeactivateAsync()
|
||||||
|
{
|
||||||
|
var key = GetCachedLicenseKey();
|
||||||
|
if (key == null) return false;
|
||||||
|
return await Client.DeactivateAsync(ProductSlug, key, ServerUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Schreibweise vereinheitlichen — die Schluessel sind durchgaengig gross.</summary>
|
||||||
|
public static string Normalize(string licenseKey) => licenseKey.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Startet die periodische Revalidierung im Hintergrund. Erkennt Widerruf und
|
||||||
|
/// Ablauf waehrend des Betriebs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="onUnusable">
|
/// <param name="onUnusable">
|
||||||
/// Wird genau einmal aufgerufen, wenn die Lizenz endgueltig nicht mehr nutzbar ist.
|
/// Wird genau einmal aufgerufen, wenn die Lizenz endgueltig nicht mehr nutzbar ist.
|
||||||
/// Der Host entscheidet, was dann passiert (Meldung anzeigen, Anwendung beenden).
|
/// Der Host entscheidet, was dann passiert. Netzwerkfehler loesen den Rueckruf
|
||||||
/// Transiente Fehler (Netz) loesen den Rueckruf nicht aus — dafuer gibt es die
|
/// nicht aus — dafuer gibt es den verschluesselten Offline-Cache des Clients.
|
||||||
/// Offline-Gnadenfrist des SDK.
|
|
||||||
/// </param>
|
/// </param>
|
||||||
public static IDisposable StartPeriodicRevalidation(LicenseClient client, Action<LicenseResult> onUnusable)
|
public static IDisposable StartPeriodicRevalidation(Action<LicenseValidationResult> onUnusable)
|
||||||
{
|
{
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
@@ -78,26 +103,39 @@ public static class LicenseGuard
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await client.RevalidateAsync();
|
var result = await ValidateCachedAsync();
|
||||||
if (IsUsable(client, result))
|
if (result == null)
|
||||||
{
|
{
|
||||||
if (result.State == LicenseState.ValidOffline)
|
Log.Warning("Lizenz-Revalidierung: kein zwischengespeicherter Schlüssel vorhanden.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
{
|
||||||
|
if (result.IsCached)
|
||||||
{
|
{
|
||||||
Log.Warning("Lizenzserver nicht erreichbar — Offline-Gnadenfrist läuft bis {GraceUntil}.",
|
Log.Warning("Lizenzserver nicht erreichbar — Lizenz gilt über den lokalen Cache weiter ({Status}).",
|
||||||
result.GraceUntil);
|
result.Status);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Fatal("Lizenzprüfung fehlgeschlagen ({State}): {Message} — Anwendung wird beendet.",
|
// network_error heisst: Server weg UND kein brauchbarer Cache.
|
||||||
result.State, result.Message);
|
// Das ist kein Widerruf — beim naechsten Durchlauf erneut versuchen.
|
||||||
|
if (result.Status.Equals("network_error", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Log.Warning("Lizenz-Revalidierung fehlgeschlagen (Netzwerk): {Message}", result.Message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Fatal("Lizenzprüfung fehlgeschlagen ({Status}): {Message} — Anwendung wird beendet.",
|
||||||
|
result.Status, result.Message);
|
||||||
onUnusable(result);
|
onUnusable(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Transiente Fehler (Netz etc.) faengt die Offline-Gnadenfrist des SDK ab —
|
// Eine unerwartete Ausnahme darf die Anwendung niemals beenden.
|
||||||
// eine unerwartete Ausnahme darf die Anwendung niemals beenden.
|
|
||||||
Log.Warning(ex, "Periodische Lizenz-Revalidierung fehlgeschlagen (wird erneut versucht).");
|
Log.Warning(ex, "Periodische Lizenz-Revalidierung fehlgeschlagen (wird erneut versucht).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,9 @@
|
|||||||
<ProjectReference Include="..\Predictalytics.Api\Predictalytics.Api.csproj" />
|
<ProjectReference Include="..\Predictalytics.Api\Predictalytics.Api.csproj" />
|
||||||
<ProjectReference Include="..\Predictalytics.Worker\Predictalytics.Worker.csproj" />
|
<ProjectReference Include="..\Predictalytics.Worker\Predictalytics.Worker.csproj" />
|
||||||
<ProjectReference Include="..\Predictalytics.Infrastructure\Predictalytics.Infrastructure.csproj" />
|
<ProjectReference Include="..\Predictalytics.Infrastructure\Predictalytics.Infrastructure.csproj" />
|
||||||
<!-- Externes Schwester-Repo: J:\Softwareprojekte\LicenseLabrador muss neben dem Predictalytics-Checkout liegen. -->
|
<!-- Externes Schwester-Repo: J:\Softwareprojekte\Deploymentcenter muss neben dem Predictalytics-Checkout liegen.
|
||||||
<ProjectReference Include="..\..\..\..\LicenseLabrador\client-dotnet\LicenseLabrador.Client\LicenseLabrador.Client.csproj" />
|
Loest LicenseLabrador ab (license.mhdf.de -> dc.mhdf.de). -->
|
||||||
|
<ProjectReference Include="..\..\..\..\Deploymentcenter\client-dotnet\Deploymentcenter.Client\Deploymentcenter.Client.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -43,12 +43,11 @@ public partial class App : global::Avalonia.Application
|
|||||||
private async Task StartupAsync(IClassicDesktopStyleApplicationLifetime desktop)
|
private async Task StartupAsync(IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
// ─── Lizenzschranke: ohne nutzbare Lizenz keine Anwendung ───
|
// ─── Lizenzschranke: ohne nutzbare Lizenz keine Anwendung ───
|
||||||
var client = LicenseGuard.CreateClient();
|
var result = await LicenseGuard.ValidateCachedAsync();
|
||||||
var result = await LicenseGuard.RevalidateAsync(client);
|
|
||||||
|
|
||||||
if (!LicenseGuard.IsUsable(client, result))
|
if (result?.IsValid != true)
|
||||||
{
|
{
|
||||||
var licenseWindow = new LicenseWindow(client, result);
|
var licenseWindow = new LicenseWindow(result);
|
||||||
desktop.MainWindow = licenseWindow;
|
desktop.MainWindow = licenseWindow;
|
||||||
licenseWindow.Show();
|
licenseWindow.Show();
|
||||||
|
|
||||||
@@ -75,11 +74,11 @@ public partial class App : global::Avalonia.Application
|
|||||||
viewModel.Start();
|
viewModel.Start();
|
||||||
|
|
||||||
// Waehrend des Betriebs alle 12 h nachpruefen (Widerruf, Ablauf, Offline-Frist).
|
// Waehrend des Betriebs alle 12 h nachpruefen (Widerruf, Ablauf, Offline-Frist).
|
||||||
_licenseWatch = LicenseGuard.StartPeriodicRevalidation(client, unusable =>
|
_licenseWatch = LicenseGuard.StartPeriodicRevalidation(unusable =>
|
||||||
Dispatcher.UIThread.Post(async () =>
|
Dispatcher.UIThread.Post(async () =>
|
||||||
{
|
{
|
||||||
await Dialogs.ShowErrorAsync(window, "Lizenzfehler",
|
await Dialogs.ShowErrorAsync(window, "Lizenzfehler",
|
||||||
$"Die Lizenz ist nicht mehr gültig ({unusable.State}):\n{unusable.Message}\n\nPredictalytics wird beendet.");
|
$"Die Lizenz ist nicht mehr gültig ({unusable.Status}):\n{unusable.Message}\n\nPredictalytics wird beendet.");
|
||||||
desktop.Shutdown();
|
desktop.Shutdown();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Api, Worker, Infrastructure und LicenseLabrador.Client kommen transitiv ueber Hosting. -->
|
<!-- Api, Worker, Infrastructure und Deploymentcenter.Client kommen transitiv ueber Hosting. -->
|
||||||
<ProjectReference Include="..\Predictalytics.Hosting\Predictalytics.Hosting.csproj" />
|
<ProjectReference Include="..\Predictalytics.Hosting\Predictalytics.Hosting.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
// Die Argumentauswertung steht bewusst vor jeder Avalonia-Initialisierung:
|
// Die Argumentauswertung steht bewusst vor jeder Avalonia-Initialisierung:
|
||||||
// auf einem Server ohne X11 wuerde der Prozess sonst beim Aufbau des
|
// auf einem Server ohne X11 wuerde der Prozess sonst beim Aufbau des
|
||||||
// Fenstersystems sterben, bevor --headless ueberhaupt greift.
|
// Fenstersystems sterben, bevor die Schalter ueberhaupt greifen.
|
||||||
|
if (LicenseCli.IsLicenseCommand(args))
|
||||||
|
{
|
||||||
|
ConsoleAttach.TryAttachToParent();
|
||||||
|
return LicenseCli.RunAsync(args).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Any(a => string.Equals(a, "--headless", StringComparison.OrdinalIgnoreCase)))
|
if (args.Any(a => string.Equals(a, "--headless", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
ConsoleAttach.TryAttachToParent();
|
ConsoleAttach.TryAttachToParent();
|
||||||
|
|||||||
@@ -29,17 +29,16 @@ internal static class HeadlessRunner
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = await EnsureLicensedAsync();
|
if (!await EnsureLicensedAsync()) return ExitNoLicense;
|
||||||
if (client == null) return ExitNoLicense;
|
|
||||||
|
|
||||||
var host = new PredictalyticsHost(options);
|
var host = new PredictalyticsHost(options);
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
using var shutdownSignals = RegisterShutdown(cts);
|
using var shutdownSignals = RegisterShutdown(cts);
|
||||||
|
|
||||||
using var licenseWatch = LicenseGuard.StartPeriodicRevalidation(client, result =>
|
using var licenseWatch = LicenseGuard.StartPeriodicRevalidation(result =>
|
||||||
{
|
{
|
||||||
Log.Fatal("Lizenz nicht mehr gültig ({State}): {Message} — Dienst wird beendet.",
|
Log.Fatal("Lizenz nicht mehr gültig ({Status}): {Message} — Dienst wird beendet.",
|
||||||
result.State, result.Message);
|
result.Status, result.Message);
|
||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,33 +75,36 @@ internal static class HeadlessRunner
|
|||||||
/// Lizenzpruefung ohne Dialog: zuerst der zwischengespeicherte Schluessel, sonst
|
/// Lizenzpruefung ohne Dialog: zuerst der zwischengespeicherte Schluessel, sonst
|
||||||
/// einmalige Aktivierung mit dem Schluessel aus der Umgebungsvariable.
|
/// einmalige Aktivierung mit dem Schluessel aus der Umgebungsvariable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<LicenseLabrador.Client.LicenseClient?> EnsureLicensedAsync()
|
private static async Task<bool> EnsureLicensedAsync()
|
||||||
{
|
{
|
||||||
var client = LicenseGuard.CreateClient();
|
var result = await LicenseGuard.ValidateCachedAsync();
|
||||||
|
if (result?.IsValid == true)
|
||||||
var result = await LicenseGuard.RevalidateAsync(client);
|
{
|
||||||
if (LicenseGuard.IsUsable(client, result)) return client;
|
Log.Information("Lizenz gültig ({Status}{Cached}).",
|
||||||
|
result.Status, result.IsCached ? ", aus lokalem Cache" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var key = Environment.GetEnvironmentVariable(LicenseKeyVariable);
|
var key = Environment.GetEnvironmentVariable(LicenseKeyVariable);
|
||||||
if (string.IsNullOrWhiteSpace(key))
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
{
|
{
|
||||||
Log.Fatal("Keine nutzbare Lizenz ({State}: {Message}) und {Variable} ist nicht gesetzt. " +
|
Log.Fatal("Keine nutzbare Lizenz ({Status}) und {Variable} ist nicht gesetzt. " +
|
||||||
"Im Headless-Betrieb gibt es keinen Aktivierungsdialog — bitte den Lizenzschlüssel " +
|
"Im Headless-Betrieb gibt es keinen Aktivierungsdialog — bitte den Lizenzschlüssel " +
|
||||||
"über die Umgebungsvariable bereitstellen.",
|
"über die Umgebungsvariable bereitstellen oder einmalig mit --license-set-key aktivieren.",
|
||||||
result.State, result.Message, LicenseKeyVariable);
|
result?.Status ?? "kein Schlüssel hinterlegt", LicenseKeyVariable);
|
||||||
return null;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("Aktiviere Lizenz mit dem Schlüssel aus {Variable}…", LicenseKeyVariable);
|
Log.Information("Aktiviere Lizenz mit dem Schlüssel aus {Variable}…", LicenseKeyVariable);
|
||||||
result = await client.ValidateAsync(key.Trim().ToUpperInvariant());
|
result = await LicenseGuard.ValidateAsync(key);
|
||||||
if (LicenseGuard.IsUsable(client, result))
|
if (result.IsValid)
|
||||||
{
|
{
|
||||||
Log.Information("Lizenz aktiviert ({State}).", result.State);
|
Log.Information("Lizenz aktiviert ({Status}).", result.Status);
|
||||||
return client;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Fatal("Aktivierung fehlgeschlagen ({State}): {Message}", result.State, result.Message);
|
Log.Fatal("Aktivierung fehlgeschlagen ({Status}): {Message}", result.Status, result.Message);
|
||||||
return null;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WatchdogHeartbeatService? StartWatchdog(PredictalyticsOptions options, PredictalyticsHost host)
|
private static WatchdogHeartbeatService? StartWatchdog(PredictalyticsOptions options, PredictalyticsHost host)
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using Predictalytics.Hosting;
|
||||||
|
|
||||||
|
namespace Predictalytics.Shell.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lizenzverwaltung von der Kommandozeile — die im Deploymentcenter-Integrations-
|
||||||
|
/// leitfaden vorgesehenen Schalter. Gedacht fuer Server ohne Oberflaeche, wo es
|
||||||
|
/// keinen Aktivierungsdialog geben kann.
|
||||||
|
/// </summary>
|
||||||
|
internal static class LicenseCli
|
||||||
|
{
|
||||||
|
public const string StatusSwitch = "--license-status";
|
||||||
|
public const string SetKeySwitch = "--license-set-key";
|
||||||
|
public const string DeactivateSwitch = "--license-deactivate";
|
||||||
|
|
||||||
|
public static bool IsLicenseCommand(string[] args) =>
|
||||||
|
args.Length > 0 && args[0] is StatusSwitch or SetKeySwitch or DeactivateSwitch;
|
||||||
|
|
||||||
|
public static async Task<int> RunAsync(string[] args)
|
||||||
|
{
|
||||||
|
switch (args[0])
|
||||||
|
{
|
||||||
|
case StatusSwitch: return await ShowStatusAsync();
|
||||||
|
case SetKeySwitch: return await SetKeyAsync(args.Length > 1 ? args[1] : null);
|
||||||
|
case DeactivateSwitch: return await DeactivateAsync();
|
||||||
|
default: return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> ShowStatusAsync()
|
||||||
|
{
|
||||||
|
var hw = LicenseGuard.GetHardwareInfo();
|
||||||
|
Console.WriteLine("Predictalytics — Lizenzstatus");
|
||||||
|
Console.WriteLine($" Produkt : {LicenseGuard.ProductSlug}");
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" Schlüssel : keiner hinterlegt");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"Aktivierung: {SetKeySwitch} <SCHLÜSSEL>");
|
||||||
|
return HeadlessRunner.ExitNoLicense;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" Schlüssel : {Mask(cachedKey)}");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.Write("Prüfe am Server… ");
|
||||||
|
|
||||||
|
var result = await LicenseGuard.ValidateCachedAsync();
|
||||||
|
Console.WriteLine(result is { IsValid: true }
|
||||||
|
? $"gültig ({result.Status}{(result.IsCached ? ", aus lokalem Cache" : "")})"
|
||||||
|
: $"NICHT gültig ({result?.Status}): {result?.Message}");
|
||||||
|
|
||||||
|
return result is { IsValid: true } ? HeadlessRunner.ExitOk : HeadlessRunner.ExitNoLicense;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> SetKeyAsync(string? key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Aufruf: {SetKeySwitch} <SCHLÜSSEL>");
|
||||||
|
return HeadlessRunner.ExitFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Write($"Aktiviere {Mask(LicenseGuard.Normalize(key))} … ");
|
||||||
|
var result = await LicenseGuard.ValidateAsync(key);
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"erfolgreich ({result.Status})");
|
||||||
|
Console.WriteLine($"Hardware-ID: {result.HardwareId}");
|
||||||
|
return HeadlessRunner.ExitOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"fehlgeschlagen ({result.Status}): {result.Message}");
|
||||||
|
return HeadlessRunner.ExitNoLicense;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> DeactivateAsync()
|
||||||
|
{
|
||||||
|
if (LicenseGuard.GetCachedLicenseKey() == null)
|
||||||
|
{
|
||||||
|
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.WriteLine(ok ? "erfolgreich" : "fehlgeschlagen");
|
||||||
|
return ok ? HeadlessRunner.ExitOk : HeadlessRunner.ExitFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nur die Segmentstruktur zeigen, nicht den vollstaendigen Schluessel.</summary>
|
||||||
|
private static string Mask(string key)
|
||||||
|
{
|
||||||
|
var parts = key.Split('-');
|
||||||
|
if (parts.Length < 2) return "*****";
|
||||||
|
for (var i = 1; i < parts.Length - 1; i++) parts[i] = new string('*', parts[i].Length);
|
||||||
|
return string.Join('-', parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="Predictalytics.Shell.Views.LicenseWindow"
|
x:Class="Predictalytics.Shell.Views.LicenseWindow"
|
||||||
Title="Predictalytics — Lizenzaktivierung"
|
Title="Predictalytics — Lizenzaktivierung"
|
||||||
Width="560" SizeToContent="Height"
|
Width="580" SizeToContent="Height"
|
||||||
CanResize="False"
|
CanResize="False"
|
||||||
WindowStartupLocation="CenterScreen">
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
<StackPanel Margin="24" Spacing="16">
|
<StackPanel Margin="24" Spacing="16">
|
||||||
|
|
||||||
<TextBlock TextWrapping="Wrap"
|
<TextBlock TextWrapping="Wrap" FontSize="15" FontWeight="SemiBold"
|
||||||
Text="Diese Installation benötigt eine gültige Lizenz." FontSize="15" FontWeight="SemiBold" />
|
Text="Diese Installation benötigt eine gültige Lizenz." />
|
||||||
|
|
||||||
<TextBlock TextWrapping="Wrap" Opacity="0.8"
|
<TextBlock TextWrapping="Wrap" Opacity="0.8"
|
||||||
Text="Bitte Lizenzschlüssel eingeben (Format: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX):" />
|
Text="Bitte Lizenzschlüssel eingeben (Format: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX):" />
|
||||||
@@ -29,5 +29,16 @@
|
|||||||
<Button Content="Beenden" MinWidth="100" IsCancel="True" Click="OnExitClick" />
|
<Button Content="Beenden" MinWidth="100" IsCancel="True" Click="OnExitClick" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Die Aktivierung wird am Server an diese Hardware-ID gebunden. -->
|
||||||
|
<Border BorderThickness="0,1,0,0" Padding="0,12,0,0"
|
||||||
|
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Hardware-ID dieser Maschine" FontSize="11" Opacity="0.6" />
|
||||||
|
<SelectableTextBlock Name="HardwareIdText"
|
||||||
|
FontFamily="Cascadia Code,DejaVu Sans Mono,Consolas,monospace"
|
||||||
|
FontSize="11" Opacity="0.8" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,39 +1,49 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using LicenseLabrador.Client;
|
using Deploymentcenter.Client;
|
||||||
using Predictalytics.Hosting;
|
using Predictalytics.Hosting;
|
||||||
|
|
||||||
namespace Predictalytics.Shell.Views;
|
namespace Predictalytics.Shell.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aktivierungsfenster, wenn keine nutzbare Lizenz vorliegt.
|
/// Aktivierungsfenster, wenn keine nutzbare Lizenz vorliegt.
|
||||||
/// <see cref="Completion"/> liefert true, sobald eine gueltige, pruefsummen-
|
/// <see cref="Completion"/> liefert true, sobald eine gueltige Lizenz vorliegt;
|
||||||
/// verifizierte Lizenz vorliegt; false, wenn der Benutzer abbricht.
|
/// false, wenn der Benutzer abbricht.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class LicenseWindow : Window
|
public partial class LicenseWindow : Window
|
||||||
{
|
{
|
||||||
private readonly LicenseClient _client = null!;
|
|
||||||
private readonly TaskCompletionSource<bool> _completion = new();
|
private readonly TaskCompletionSource<bool> _completion = new();
|
||||||
|
|
||||||
/// <summary>Wird abgeschlossen, wenn aktiviert oder abgebrochen wurde.</summary>
|
/// <summary>Wird abgeschlossen, wenn aktiviert oder abgebrochen wurde.</summary>
|
||||||
public Task<bool> Completion => _completion.Task;
|
public Task<bool> Completion => _completion.Task;
|
||||||
|
|
||||||
public LicenseWindow() => InitializeComponent();
|
public LicenseWindow()
|
||||||
|
|
||||||
public LicenseWindow(LicenseClient client, LicenseResult? lastResult) : this()
|
|
||||||
{
|
{
|
||||||
_client = client;
|
InitializeComponent();
|
||||||
|
|
||||||
if (lastResult != null && lastResult.State != LicenseState.NoLicense)
|
try
|
||||||
{
|
{
|
||||||
SetStatus($"Letzte Prüfung: {lastResult.State} — {lastResult.Message}", isError: true);
|
var hw = LicenseGuard.GetHardwareInfo();
|
||||||
|
HardwareIdText.Text = $"{hw.HardwareId}\nQuelle: {hw.HwidSource}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HardwareIdText.Text = $"nicht ermittelbar: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LicenseWindow(LicenseValidationResult? lastResult) : this()
|
||||||
|
{
|
||||||
|
if (lastResult != null && !string.IsNullOrWhiteSpace(lastResult.Message))
|
||||||
|
{
|
||||||
|
SetStatus($"Letzte Prüfung: {lastResult.Status} — {lastResult.Message}", isError: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnActivateClick(object? sender, RoutedEventArgs e)
|
private async void OnActivateClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var key = KeyBox.Text?.Trim().ToUpperInvariant() ?? "";
|
var key = KeyBox.Text ?? "";
|
||||||
if (string.IsNullOrWhiteSpace(key))
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
{
|
{
|
||||||
SetStatus("Bitte einen Lizenzschlüssel eingeben.", isError: true);
|
SetStatus("Bitte einen Lizenzschlüssel eingeben.", isError: true);
|
||||||
@@ -46,15 +56,15 @@ public partial class LicenseWindow : Window
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _client.ValidateAsync(key);
|
var result = await LicenseGuard.ValidateAsync(key);
|
||||||
if (LicenseGuard.IsUsable(_client, result))
|
if (result.IsValid)
|
||||||
{
|
{
|
||||||
_completion.TrySetResult(true);
|
_completion.TrySetResult(true);
|
||||||
Close();
|
Close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetStatus($"Lizenz nicht nutzbar ({result.State}):\n{result.Message}", isError: true);
|
SetStatus($"Lizenz nicht nutzbar ({result.Status}):\n{result.Message}", isError: true);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Api, Worker, Infrastructure und LicenseLabrador.Client kommen transitiv ueber Hosting. -->
|
<!-- Api, Worker, Infrastructure und Deploymentcenter.Client kommen transitiv ueber Hosting. -->
|
||||||
<ProjectReference Include="..\Predictalytics.Hosting\Predictalytics.Hosting.csproj" />
|
<ProjectReference Include="..\Predictalytics.Hosting\Predictalytics.Hosting.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ internal static class Program
|
|||||||
ApplicationConfiguration.Initialize();
|
ApplicationConfiguration.Initialize();
|
||||||
|
|
||||||
// ─── License gate: no usable license, no app ───
|
// ─── License gate: no usable license, no app ───
|
||||||
var licenseClient = LicenseGate.EnsureLicensed();
|
if (!LicenseGate.EnsureLicensed())
|
||||||
if (licenseClient == null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -28,13 +27,13 @@ internal static class Program
|
|||||||
mainForm.Initialize();
|
mainForm.Initialize();
|
||||||
|
|
||||||
// While running: re-check the license every 12 h (revocation/expiry/offline grace).
|
// While running: re-check the license every 12 h (revocation/expiry/offline grace).
|
||||||
using var licenseWatch = LicenseGuard.StartPeriodicRevalidation(licenseClient, result =>
|
using var licenseWatch = LicenseGuard.StartPeriodicRevalidation(result =>
|
||||||
{
|
{
|
||||||
if (mainForm.IsDisposed) return;
|
if (mainForm.IsDisposed) return;
|
||||||
mainForm.BeginInvoke(() =>
|
mainForm.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
$"Die Lizenz ist nicht mehr gültig ({result.State}):\n{result.Message}\n\nPredictalytics wird beendet.",
|
$"Die Lizenz ist nicht mehr gültig ({result.Status}):\n{result.Message}\n\nPredictalytics wird beendet.",
|
||||||
"Lizenzfehler", MessageBoxButtons.OK, MessageBoxIcon.Stop);
|
"Lizenzfehler", MessageBoxButtons.OK, MessageBoxIcon.Stop);
|
||||||
System.Windows.Forms.Application.Exit();
|
System.Windows.Forms.Application.Exit();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,44 +1,40 @@
|
|||||||
using LicenseLabrador.Client;
|
using Deploymentcenter.Client;
|
||||||
|
using Predictalytics.Hosting;
|
||||||
|
|
||||||
namespace Predictalytics.WinFormsHost.Services;
|
namespace Predictalytics.WinFormsHost.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Modal dialog shown at startup when no usable license is present.
|
/// Modal dialog shown at startup when no usable license is present.
|
||||||
/// Lets the user enter/activate a license key; closes with OK only after a
|
/// Lets the user enter/activate a license key; closes with OK only after a
|
||||||
/// successful, checksum-verified validation.
|
/// successful validation against the Deploymentcenter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LicenseDialog : Form
|
public sealed class LicenseDialog : Form
|
||||||
{
|
{
|
||||||
private readonly LicenseClient _client;
|
|
||||||
private readonly Label _lblStatus;
|
private readonly Label _lblStatus;
|
||||||
private readonly TextBox _txtKey;
|
private readonly TextBox _txtKey;
|
||||||
private readonly Button _btnActivate;
|
private readonly Button _btnActivate;
|
||||||
private readonly Button _btnExit;
|
private readonly Button _btnExit;
|
||||||
|
|
||||||
public LicenseResult? Result { get; private set; }
|
public LicenseDialog(LicenseValidationResult? lastResult)
|
||||||
|
|
||||||
public LicenseDialog(LicenseClient client, LicenseResult? lastResult)
|
|
||||||
{
|
{
|
||||||
_client = client;
|
|
||||||
|
|
||||||
Text = "Predictalytics — Lizenzaktivierung";
|
Text = "Predictalytics — Lizenzaktivierung";
|
||||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
MinimizeBox = false;
|
MinimizeBox = false;
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
ClientSize = new Size(460, 190);
|
ClientSize = new Size(480, 250);
|
||||||
|
|
||||||
var lblInfo = new Label
|
var lblInfo = new Label
|
||||||
{
|
{
|
||||||
Text = "Diese Installation benötigt eine gültige Lizenz.\nBitte Lizenzschlüssel eingeben (Format: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX):",
|
Text = "Diese Installation benötigt eine gültige Lizenz.\nBitte Lizenzschlüssel eingeben (Format: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX):",
|
||||||
Location = new Point(12, 12),
|
Location = new Point(12, 12),
|
||||||
Size = new Size(436, 34)
|
Size = new Size(456, 34)
|
||||||
};
|
};
|
||||||
|
|
||||||
_txtKey = new TextBox
|
_txtKey = new TextBox
|
||||||
{
|
{
|
||||||
Location = new Point(12, 52),
|
Location = new Point(12, 52),
|
||||||
Size = new Size(436, 26),
|
Size = new Size(456, 26),
|
||||||
Font = new Font("Consolas", 11f),
|
Font = new Font("Consolas", 11f),
|
||||||
CharacterCasing = CharacterCasing.Upper
|
CharacterCasing = CharacterCasing.Upper
|
||||||
};
|
};
|
||||||
@@ -46,36 +42,59 @@ public sealed class LicenseDialog : Form
|
|||||||
_lblStatus = new Label
|
_lblStatus = new Label
|
||||||
{
|
{
|
||||||
Location = new Point(12, 84),
|
Location = new Point(12, 84),
|
||||||
Size = new Size(436, 50),
|
Size = new Size(456, 50),
|
||||||
ForeColor = Color.Firebrick,
|
ForeColor = Color.Firebrick,
|
||||||
Text = FormatInitialStatus(lastResult)
|
Text = FormatInitialStatus(lastResult)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Die Aktivierung wird am Server an diese Hardware-ID gebunden.
|
||||||
|
var lblHardwareId = new Label
|
||||||
|
{
|
||||||
|
Location = new Point(12, 140),
|
||||||
|
Size = new Size(456, 60),
|
||||||
|
ForeColor = Color.Gray,
|
||||||
|
Font = new Font("Consolas", 7.5f),
|
||||||
|
Text = FormatHardwareId()
|
||||||
|
};
|
||||||
|
|
||||||
_btnActivate = new Button
|
_btnActivate = new Button
|
||||||
{
|
{
|
||||||
Text = "Aktivieren",
|
Text = "Aktivieren",
|
||||||
Location = new Point(252, 146),
|
Location = new Point(262, 206),
|
||||||
Size = new Size(96, 30)
|
Size = new Size(100, 30)
|
||||||
};
|
};
|
||||||
_btnActivate.Click += async (_, _) => await ActivateAsync();
|
_btnActivate.Click += async (_, _) => await ActivateAsync();
|
||||||
|
|
||||||
_btnExit = new Button
|
_btnExit = new Button
|
||||||
{
|
{
|
||||||
Text = "Beenden",
|
Text = "Beenden",
|
||||||
Location = new Point(354, 146),
|
Location = new Point(368, 206),
|
||||||
Size = new Size(94, 30),
|
Size = new Size(100, 30),
|
||||||
DialogResult = DialogResult.Cancel
|
DialogResult = DialogResult.Cancel
|
||||||
};
|
};
|
||||||
|
|
||||||
AcceptButton = _btnActivate;
|
AcceptButton = _btnActivate;
|
||||||
CancelButton = _btnExit;
|
CancelButton = _btnExit;
|
||||||
Controls.AddRange(new Control[] { lblInfo, _txtKey, _lblStatus, _btnActivate, _btnExit });
|
Controls.AddRange([lblInfo, _txtKey, _lblStatus, lblHardwareId, _btnActivate, _btnExit]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatInitialStatus(LicenseResult? lastResult)
|
private static string FormatInitialStatus(LicenseValidationResult? lastResult)
|
||||||
{
|
{
|
||||||
if (lastResult == null || lastResult.State == LicenseState.NoLicense) return "";
|
if (lastResult == null || string.IsNullOrWhiteSpace(lastResult.Message)) return "";
|
||||||
return $"Letzte Prüfung: {lastResult.State} — {lastResult.Message}";
|
return $"Letzte Prüfung: {lastResult.Status} — {lastResult.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatHardwareId()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hw = LicenseGuard.GetHardwareInfo();
|
||||||
|
return $"Hardware-ID: {hw.HardwareId}\nQuelle: {hw.HwidSource}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return $"Hardware-ID nicht ermittelbar: {ex.Message}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ActivateAsync()
|
private async Task ActivateAsync()
|
||||||
@@ -93,17 +112,16 @@ public sealed class LicenseDialog : Form
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _client.ValidateAsync(key);
|
var result = await LicenseGuard.ValidateAsync(key);
|
||||||
if (result.IsUsable && _client.VerifyChecksum(result))
|
if (result.IsValid)
|
||||||
{
|
{
|
||||||
Result = result;
|
|
||||||
DialogResult = DialogResult.OK;
|
DialogResult = DialogResult.OK;
|
||||||
Close();
|
Close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_lblStatus.ForeColor = Color.Firebrick;
|
_lblStatus.ForeColor = Color.Firebrick;
|
||||||
_lblStatus.Text = $"Lizenz nicht nutzbar ({result.State}):\n{result.Message}";
|
_lblStatus.Text = $"Lizenz nicht nutzbar ({result.Status}):\n{result.Message}";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using LicenseLabrador.Client;
|
|
||||||
using Predictalytics.Hosting;
|
using Predictalytics.Hosting;
|
||||||
|
|
||||||
namespace Predictalytics.WinFormsHost.Services;
|
namespace Predictalytics.WinFormsHost.Services;
|
||||||
@@ -12,19 +11,17 @@ internal static class LicenseGate
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Blockiert, bis eine nutzbare Lizenz vorliegt. Zuerst wird der zwischengespeicherte
|
/// Blockiert, bis eine nutzbare Lizenz vorliegt. Zuerst wird der zwischengespeicherte
|
||||||
/// Schluessel geprueft; erst wenn der nicht (mehr) nutzbar ist, erscheint der Dialog.
|
/// Schluessel geprueft; erst wenn der nicht (mehr) nutzbar ist, erscheint der Dialog.
|
||||||
/// Liefert null, wenn der Benutzer abbricht — die Anwendung muss dann beendet werden.
|
/// Liefert false, wenn der Benutzer abbricht — die Anwendung muss dann beendet werden.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static LicenseClient? EnsureLicensed()
|
public static bool EnsureLicensed()
|
||||||
{
|
{
|
||||||
var client = LicenseGuard.CreateClient();
|
var result = LicenseGuard.ValidateCachedAsync().GetAwaiter().GetResult();
|
||||||
|
if (result?.IsValid == true)
|
||||||
var result = LicenseGuard.RevalidateAsync(client).GetAwaiter().GetResult();
|
|
||||||
if (LicenseGuard.IsUsable(client, result))
|
|
||||||
{
|
{
|
||||||
return client;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dialog = new LicenseDialog(client, result);
|
using var dialog = new LicenseDialog(result);
|
||||||
return dialog.ShowDialog() == DialogResult.OK ? client : null;
|
return dialog.ShowDialog() == DialogResult.OK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user