Merge von main: die dort entstandene Deployment-Center-Integration (Lizenz, Heartbeat mit DB-Health-Check, Fehler-Stream, UpdateService) ist jetzt Teil des plattformneutralen Kerns. Predictalytics.WinFormsHost ist entfernt. Nach Predictalytics.Hosting gezogen: - DcConfig, DcApiClient, DcErrorSink, DcHeartbeatService, DcUpdateService unveraendert - sie waren bereits plattformneutral - DcErrorReporter ohne Application.ThreadException und MessageBox; der UI-Thread-Handler liegt jetzt beim Host und ruft ReportUiThreadException - LicenseGuard/LicenseSession ohne Dialog und ohne WinForms-Timer. Neu: TryUseCachedAsync, ActivateAsync, StartPeriodicRevalidation ueber PeriodicTimer. Die Unterscheidung transienter Fehler und die Warnung vor ablaufender Gnadenfrist sind unveraendert uebernommen. - Dc-Einstellungen von AppSettings nach PredictalyticsOptions; die Watchdog-Einstellungen entfallen - DcErrorSink im LoggingSetup, Startbanner nutzt DcConfig.AppVersion BuildInfo.targets wird jetzt von Predictalytics.Hosting importiert. In der Avalonia-Shell nachgezogen: - Menue Deployment Center mit Update-Suche und Lizenzstatus - Einstellungsgruppe Deployment Center statt Watchdog, Update-Kanal als ComboBox, Server-URL nur zur Anzeige - Heartbeat-Snapshot mit SELECT-1-Probe wie in der WinForms-Fassung - Update-Pruefung still beim Start und interaktiv ueber das Menue, mit NotifyStopping vor dem Start des Update-Agenten - Lizenzfenster wertet IsTransient aus: bei fehlender Serververbindung wird nicht behauptet, die Lizenz sei ungueltig - TextBox.Watermark auf PlaceholderText (in Avalonia 12 veraltet) Build: 0 Fehler, 8 Warnungen (alle vorbestehend). Tests: 100 bestanden, 0 Fehler, 1 uebersprungen. Verifiziert: --license-status meldet gueltig samt Gnadenfrist; die GUI startet durch, prueft die Lizenz und laeuft gegen den UpdateService. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
4.9 KiB
C#
124 lines
4.9 KiB
C#
using Deploymentcenter.Client;
|
|
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)
|
|
{
|
|
LicenseClient.DefaultAppVersion = DcConfig.AppVersion;
|
|
|
|
return args[0] switch
|
|
{
|
|
StatusSwitch => await ShowStatusAsync(),
|
|
SetKeySwitch => await SetKeyAsync(args.Length > 1 ? args[1] : null),
|
|
DeactivateSwitch => await DeactivateAsync(),
|
|
_ => HeadlessRunner.ExitFailed
|
|
};
|
|
}
|
|
|
|
private static async Task<int> ShowStatusAsync()
|
|
{
|
|
var hw = LicenseGuard.GetHardwareInfo();
|
|
Console.WriteLine("Predictalytics — Lizenzstatus");
|
|
Console.WriteLine($" Produkt : {DcConfig.ProductSlug}");
|
|
Console.WriteLine($" Version : {DcConfig.AppVersion} ({DcConfig.GitCommitShort})");
|
|
Console.WriteLine($" Server : {DcConfig.BaseUrl}");
|
|
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 = LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
|
|
if (string.IsNullOrWhiteSpace(cachedKey))
|
|
{
|
|
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 Deployment Center… ");
|
|
|
|
var check = await LicenseGuard.TryUseCachedAsync();
|
|
var result = check.LastResult;
|
|
|
|
if (check.IsUsable)
|
|
{
|
|
Console.WriteLine($"gültig ({result!.Status}{(result.IsCached ? ", aus lokalem Cache" : "")})");
|
|
if (result.CacheExpiresAt is { } expiresAt && expiresAt > 0)
|
|
{
|
|
Console.WriteLine($" Offline-Gnadenfrist bis: {DateTimeOffset.FromUnixTimeSeconds(expiresAt).UtcDateTime:yyyy-MM-dd HH:mm} UTC");
|
|
}
|
|
return HeadlessRunner.ExitOk;
|
|
}
|
|
|
|
// Transient heisst: kein Urteil, nur eine gescheiterte Verbindung.
|
|
Console.WriteLine(result?.IsTransient == true
|
|
? $"unentschieden ({result.Status}): {result.Message}"
|
|
: $"NICHT gültig ({result?.Status}): {result?.Message}");
|
|
return 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(key.Trim().ToUpperInvariant())} … ");
|
|
var (session, result) = await LicenseGuard.ActivateAsync(LicenseGuard.CreateClient(), key);
|
|
|
|
if (session is not null)
|
|
{
|
|
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()
|
|
{
|
|
var key = LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
{
|
|
Console.Error.WriteLine("Kein hinterlegter Schlüssel — nichts zu deaktivieren.");
|
|
return HeadlessRunner.ExitFailed;
|
|
}
|
|
|
|
Console.Write("Gebe Aktivierungsplatz am Deployment Center frei… ");
|
|
var ok = await LicenseGuard.CreateClient()
|
|
.DeactivateAsync(DcConfig.ProductSlug, key!, DcConfig.BaseUrl);
|
|
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);
|
|
}
|
|
}
|