Ein .NET-CLI-Tool lizenzieren: Headless-Aktivierung, KEYRIGHT_LICENSE und Runner ohne „Registrieren“-Dialog
Ein Kommandozeilenwerkzeug hat keinen Einstellungsbildschirm, läuft häufig auf einer Maschine, vor der kein Mensch sitzt, und wird von Skripten gesteuert, die nur Exit-Codes lesen. Das sprengt das Desktop-Lizenzierungsskript — Schlüssel in einen Dialog einfügen, Lease zwischenspeichern, weitermachen. Hier erfahren Sie, wie Sie ein dotnet-Tool oder einen Build-Task mit Keyright ohne UI lizenzieren: ein `license`-Unterbefehl, der aktiviert und berichtet, die sechs Orte, an denen das SDK nach einer Lizenz sucht, und welches CLI-Idiom auf welchen davon abbildet, Fail-closed-Gating, das niemals nachfragt, wenn stdin kein Terminal ist, und das Problem flüchtiger CI-Runner, gelöst mit einer Offline-Lizenzdatei, die in KEYRIGHT_LICENSE mitgeführt wird, statt mit einem Seat pro Job.
Lizenzierungsanleitungen sind für Anwendungen mit einem Fenster geschrieben. Schlüssel in einen Dialog einfügen, die App ruft ActivateAsync auf, eine Lease landet in App-Data, und Validate() hält die App offline lizenziert, bis die Lease aufgefrischt werden muss. Ein CLI-Tool — ein dotnet tool, ein Build-Task, ein Code-Generator, ein Deploy-Skript — bricht drei Annahmen dieser Geschichte auf einmal:
- Es gibt keinen Dialog. Die einzige Eingabeoberfläche sind Argumente, Umgebungsvariablen und Dateien.
- Auf der Maschine sitzt oft kein Mensch. Ein CI-Runner, ein Container, ein geplanter Job.
- Der Aufrufer ist ein Skript, das Exit-Codes und stderr liest, keine Person, die eine Meldungsbox liest.
Nichts davon ändert, wie Keyright eine Lizenz verifiziert — dasselbe signierte Dokument, derselbe eingebettete öffentliche Schlüssel, dasselbe Offline-Validate(). Was sich ändert, ist, woher die Lizenz kommt und wie das Tool darüber berichtet. Dieser Beitrag baut diese Schicht für ein Tool namens mytool.
Woher die Lizenz eines CLI kommt
Das Keyright-SDK löst eine Lizenz aus mehreren Quellen in einer festen Rangfolge auf, höchste zuerst: ein expliziter LicenseString, ein expliziter LicenseFilePath, die Datei, die die Umgebungsvariable KEYRIGHT_LICENSE benennt, ein ConfigLicensePath, der App-Data-Speicherort des Betriebssystems ({LocalApplicationData}/Keyright/{product}/license.json) und schließlich die zwischengespeicherte Aktivierungs-Lease, die ein früheres ActivateAsync geschrieben hat. Eine Desktop-App setzt nichts davon. Ein CLI bildet jede dieser Quellen auf ein Idiom ab, das seine Benutzer bereits kennen:
--license <path>auf der Kommandozeile entsprichtLicenseFilePath. Es schlägt alles andere, was genau das ist, was Sie für „führe diesen einen Aufruf mit jener Datei aus“ wollen.KEYRIGHT_LICENSEist der Pfad aus der Umgebungsvariable. Das ist das CI- und Container-Idiom: Wer den Job bereitstellt, schreibt eine Datei und exportiert die Variable; das Tool selbst tut nichts Besonderes.- Der App-Data-Speicherort ist der Ort, an dem ein Benutzer eine Lizenzdatei von Hand ablegen kann — nützlich für den Kunden im abgeschotteten Netz, der
license.jsonper E-Mail erhalten hat. - Die zwischengespeicherte Lease ist das, was
mytool license activateauf einer Entwickler-Workstation erzeugt. Das ist der Online-Pfad, und nach der einen Aktivierung ist er genauso offline wie die anderen.
Initialisieren Sie den Client einmal und lassen Sie das Flag die Option speisen, wenn es vorhanden ist:
using Keyright.Client;
static KeyrightClient CreateClient(string? licensePath) =>
KeyrightClient.Initialize(new KeyrightOptions
{
Product = "mytool",
PublicKeyBase64 = "MIIBIjANBgkq...", // verify-only; safe to embed
ServiceUrl = "https://licensing.acme.example", // your issuing service
LicenseFilePath = licensePath, // --license, or null to fall through
});
PublicKeyBase64 ist das einzige Schlüsselmaterial in der Binärdatei, und es kann nur verifizieren — aus einem dekompilierten CLI lässt sich nichts stehlen, womit jemand eine Lizenz ausstellen könnte.
Der license-Unterbefehl
Alles, was ein Dialog getan hat, wird zu einem Verb. Mit System.CommandLine:
var licenseOpt = new Option<string?>("--license", "Path to a license file for this run");
var activate = new Command("activate", "Bind this machine to a license key");
var keyArg = new Argument<string>("key");
activate.AddArgument(keyArg);
activate.SetHandler(async (string key, string? path) =>
{
var client = CreateClient(path);
LicenseInfo info = await client.ActivateAsync(key, CancellationToken.None);
if (info.IsValid && info.IsPaid)
{
Console.WriteLine($"Activated: {info.StatusBadge}. Lease cached; the tool now runs offline.");
return;
}
Console.Error.WriteLine($"mytool: activation failed — {info.Message}");
Environment.ExitCode = info.Status == LicenseStatus.SeatLimit ? 3 : 2;
}, keyArg, licenseOpt);
var status = new Command("status", "Show the resolved license and where it came from");
status.SetHandler((string? path) =>
{
var client = CreateClient(path);
LicenseInfo info = client.Validate(out LicenseSourceKind source);
Console.WriteLine($"edition : {info.StatusBadge}");
Console.WriteLine($"status : {info.Status}");
Console.WriteLine($"source : {source}");
if (info.IsValid) Console.WriteLine($"expires : {info.ExpiryUtc:u}");
else Console.WriteLine($"reason : {info.Message}");
Environment.ExitCode = info.IsValid ? 0 : 2;
}, licenseOpt);
var license = new Command("license") { activate, status };
license.AddGlobalOption(licenseOpt);
ActivateAsync sendet den Schlüssel samt einer stabilen Maschinen-ID an Ihren Dienst, der einen Seat verbraucht und eine signierte Lease zurückgibt, die das SDK verifiziert und zwischenspeichert. Für die gewöhnlichen Fehlschläge — ungültiger Schlüssel, Seat-Limit, widerrufen, nicht erreichbar — wirft es nicht, sondern gibt ein fail-closed LicenseInfo zurück, dessen Status und Message den Grund nennen. Genau das will ein CLI: einen Wert, der sich auf einen Exit-Code abbilden lässt, statt einer Ausnahme, die abgefangen werden muss.
status ist das Stück, das Desktop-Apps selten brauchen und ohne das CLIs nicht auskommen. Validate(out LicenseSourceKind source) verrät Ihnen, welche Quelle gewonnen hat. Wenn ein Benutzer also sagt „auf meiner Maschine ist es lizenziert, in der Pipeline aber nicht“, machen die beiden Ausgaben den Unterschied offensichtlich: Die eine hat die zwischengespeicherte Lease aufgelöst, die andere nichts, weil KEYRIGHT_LICENSE nicht exportiert war.
Dokumentieren Sie die Exit-Codes neben den Befehlen. Eine Konvention, die sich bewährt hat: 0 lizenziert, 2 keine gültige Lizenz, 3 Seat-Limit, 4 lizenziert, aber die Berechtigung, die dieser Befehl benötigt, fehlt. Skripte verzweigen darauf; Menschen lesen die eine Zeile auf stderr.
Einen Befehl sperren, ohne jemals nachzufragen
Die eigentliche Sperre ist dieselbe wie überall sonst — Validate(), offline, fail-closed —, aber die Berichterstattung muss wissen, wer zuhört:
static int RequireEntitlement(KeyrightClient client, string entitlement)
{
LicenseInfo info = client.Validate(); // offline; never throws
if (info.IsValid && info.Entitlements.IsEnabled(entitlement))
return 0;
bool interactive = !Console.IsInputRedirected
&& Environment.GetEnvironmentVariable("CI") is null;
string fix = info.IsValid
? $"this command needs the '{entitlement}' entitlement — upgrade at https://acme.example/pricing"
: "no valid license — run `mytool license activate <key>` or set KEYRIGHT_LICENSE";
Console.Error.WriteLine($"mytool: {fix}");
if (!interactive) return info.IsValid ? 4 : 2;
// Only a human gets a second line; still no modal, still no blocking prompt.
Console.Error.WriteLine("mytool: run `mytool license status` for details");
return info.IsValid ? 4 : 2;
}
// in the command handler
int gate = RequireEntitlement(client, "parallel-build");
if (gate != 0) return gate;
IsEnabled ist fail-closed — eine unbekannte Berechtigung gilt als deaktiviert —, sodass ein Tippfehler im Berechtigungsnamen einen gesperrten Befehl ergibt, niemals einen versehentlich freigeschalteten. Numerische Limits funktionieren genauso: client.GetLimit("max-projects", fallback: 1) liefert Ihren konservativen Fallback für einen nicht lizenzierten Aufrufer und ein Maximum für "unlimited".
Blockieren Sie niemals mit Console.ReadLine(), um nach einem Schlüssel zu fragen, wenn die Lizenz fehlt. In einer nicht interaktiven Sitzung bleibt der Job dann hängen, bis der Timeout der Pipeline ihn abschießt, was ein klares „nicht lizenziert“ in ein rätselhaftes „der Build lief 60 Minuten und starb“ verwandelt. Die Prüfung auf Console.IsInputRedirected plus die Variable CI (die jedes große CI-System setzt) ist eine günstige Versicherung.
Das Problem der flüchtigen Runner
Hier ist die Falle, die speziell Tools betrifft, die in CI laufen. Die Online-Aktivierung ist node-locked: Das SDK nimmt einen Fingerabdruck der Maschine, der Dienst bindet einen Seat an diesen Fingerabdruck, und das erneute Aktivieren derselben Maschine ist idempotent. Auf einer Entwickler-Workstation ist das ideal — ein Seat, eine Box, für immer. Auf einem gehosteten CI-Runner ist die „Box“ für jeden Job eine frische VM oder ein frischer Container, also präsentiert jeder Job einen neuen Fingerabdruck, jeder Job verbraucht einen neuen Seat, und nach N Jobs erreicht die Lizenz ihr Seat-Limit und Ihr Build scheitert um 3 Uhr nachts mit SeatLimit.
Die Lösung ist nicht eine höhere Seat-Zahl. Sie besteht darin, den Bereitstellungspfad zu nutzen, der keine Seat-Verfolgung hat: eine eigenständige signierte Lizenzdatei. Sie signieren sie einmal für den Kunden (oder für Ihre eigene CI, wenn das Tool intern ist):
keyright license --sign --licensee "Acme Corp" --product mytool --tier pro \
--private-key mytool.private.json --expiry 2027-12-31 --out mytool.license.json
Der Kunde legt diese Datei in seinem CI-Secret-Store ab und materialisiert sie pro Job. In GitHub Actions:
- name: Install license
run: |
printf '%s' "$MYTOOL_LICENSE_JSON" > "$RUNNER_TEMP/mytool.license.json"
echo "KEYRIGHT_LICENSE=$RUNNER_TEMP/mytool.license.json" >> "$GITHUB_ENV"
env:
MYTOOL_LICENSE_JSON: ${{ secrets.MYTOOL_LICENSE_JSON }}
- name: Build
run: mytool build --parallel # Validate() finds KEYRIGHT_LICENSE, verifies offline
Kein Aktivierungsaufruf, kein Netzwerk, kein Seat. Validate() verifiziert die RSA-Signatur der Datei gegen den eingebetteten öffentlichen Schlüssel, prüft Produkt, Ablaufdatum und die optionale mit ausgelieferte Sperrliste, und der Befehl läuft. Die Datei ist die eigene Berechtigung des Kunden — sie gewährt nichts über das Gekaufte hinaus —, aber sie ist übertragbar und gehört deshalb in den Secret-Speicher, nicht ins Repository.
Was Sie gegenüber der Online-Aktivierung aufgeben, ist der Live-Widerruf. Eine eigenständige Datei lässt sich nur über eine Sperrliste abschneiden, die Sie mit dem nächsten Release des Tools ausliefern, und eine Seat-Zählung wird nicht durchgesetzt. Für ein Build-Tool ist dieser Tausch fast immer richtig: Das Ablaufdatum der Datei begrenzt das Risiko, und CI-Jobs als Seats zu zählen, war ohnehin nie Ihr Plan.
Behalten Sie die Online-Aktivierung für die Maschinen, bei denen ein Seat wirklich eine Box bedeutet: Entwickler-Workstations und persistente selbst gehostete Runner, die ihren Fingerabdruck zwischen Jobs behalten. Diese führen mytool license activate einmal aus; ein erneutes Aktivieren bei späteren Läufen, wenn das Netzwerk erreichbar ist, frischt die Lease stillschweigend auf und verbraucht niemals einen zweiten Seat, sodass das Tool ActivateAsync beim Start opportunistisch aufrufen und offline auf die zwischengespeicherte Lease zurückfallen kann.
Ein Tool, zwei Arten von Maschinen
Setzen Sie die Teile zusammen, und das Tool hat eine einzige, einfache Richtlinie:
- Auflösung:
--licenseschlägtKEYRIGHT_LICENSEschlägt die App-Data-Datei schlägt die zwischengespeicherte Lease.mytool license statuszeigt, welche gewonnen hat. - Workstations: einmal
mytool license activate <key>; node-locked Seat; danach offline; opportunistisches Auffrischen. - Wegwerf-Runner: eine signierte Lizenzdatei aus dem Secret-Store über
KEYRIGHT_LICENSE; kein Seat, kein Netzwerk, Widerruf per Liste, begrenzt durch das Ablaufdatum. - Gating:
Validate()+IsEnabled, fail-closed, keine Rückfragen, dokumentierte Exit-Codes, eine Zeile auf stderr, die die Abhilfe nennt.
Die Verifikation darunter hat sich nie geändert. Ihre Benutzer brauchen nur kein Fenster mehr, um sie zu erreichen. Siehe Offline- und abgeschottete Lizenzierung für den Lizenzdatei-Pfad im Detail, Aktivierungslebenszyklus für das Lease-Modell, das das Verb activate antreibt, und containerisierte Dienste lizenzieren für das verwandte Problem autoskalierter Replikate.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.