License grace period: keep running when the master is down or rejects the key (LicenseGraceHours, default 72 h), start with the last confirmed license, auto-retry; panel restart button (port/panel settings without program restart, login kept); tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:02:05 +02:00
co-authored by Claude Opus 5.5
parent d4e5897b7f
commit 14db3ef4f4
10 changed files with 325 additions and 24 deletions
+63 -9
View File
@@ -19,6 +19,7 @@ public sealed class ServerHost
static readonly TimeSpan CrashWindow = TimeSpan.FromMinutes(10);
static readonly TimeSpan CrashRestartDelay = TimeSpan.FromSeconds(10);
static readonly TimeSpan MasterRetryDelay = TimeSpan.FromSeconds(30);
static readonly TimeSpan LicenseRetryDelay = TimeSpan.FromMinutes(5);
readonly string dataDir, configPath;
readonly bool offline;
@@ -87,6 +88,30 @@ public sealed class ServerHost
return "Stopping the server - the panel keeps running";
}
/// <summary>server.json für einen Panel-Neustart lesen (Umgebungsvariablen gelten weiter).</summary>
public ServerConfig LoadConfigForRestart()
{
if (offline) return Config;
try
{
var fresh = ServerConfig.LoadOrCreate(configPath, out _);
fresh.ApplyEnvironment();
return fresh;
}
catch { return null; }
}
/// <summary>Panel mit neuen Einstellungen neu starten; geänderter Spiel-Port: Gameserver neu starten bzw. Port neu binden.</summary>
public void RestartPanel(ServerConfig fresh, string by)
{
bool portChanged = fresh.Port != listenerPort;
Config = fresh;
Panel?.RestartWeb(fresh);
if (!portChanged) return;
if (Server != null && State == HostState.Running) RequestRestart(by); // bindet beim Start den neuen Port
else Listen(fresh.Port);
}
public string RequestRestart(string by)
{
var server = Server;
@@ -146,6 +171,14 @@ public sealed class ServerHost
Server = null;
if (server.ExitRequested) break;
if (server.LicenseStopReason != null)
{
// Lizenz zu lange nicht bestätigt: regelmäßig neu versuchen - startet von selbst, sobald der Master wieder bestätigt
SetState(HostState.Error, server.LicenseStopReason);
startRequested = true;
startAt = DateTime.UtcNow + LicenseRetryDelay;
continue;
}
if (server.RestartRequested)
{
GameServer.Log("Restarting the server ...");
@@ -220,23 +253,43 @@ public sealed class ServerHost
}
string owner = "Test";
DateTime confirmed = DateTime.UtcNow;
if (!offline)
{
if (string.IsNullOrWhiteSpace(Config.LicenseKey))
return Fail("No license key set - get one at " + MasterServer.Url + "/keys and enter it in the panel (Settings) or server.json");
GameServer.Log($"Checking license key at {MasterServer.Url} ...");
var key = new MasterClient(MasterServer.Url).ValidateKeyAsync(Config.LicenseKey).GetAwaiter().GetResult();
if (key.Unreachable)
if (key.Valid)
{
// Master gerade nicht erreichbar: gleich nochmal versuchen, bis der Server gestoppt wird
startRequested = true;
startAt = DateTime.UtcNow + MasterRetryDelay;
return Fail($"Master server not reachable ({key.Error}) - retrying in {MasterRetryDelay.TotalSeconds:0} s");
GameServer.Log($"License key OK ({key.Label}, owner {key.Owner})");
owner = key.Owner;
LicenseCache.Save(dataDir, Config.LicenseKey, key.Owner, key.Label, confirmed);
}
else
{
// Master nicht erreichbar oder lehnt ab: mit der letzten Bestätigung starten, solange sie frisch genug ist
var grace = TimeSpan.FromHours(Math.Clamp(Config.LicenseGraceHours, 1, 24 * 30));
var cached = LicenseCache.Load(dataDir, Config.LicenseKey);
string problem = key.Unreachable ? $"Master server not reachable ({key.Error})" : "License key rejected: " + (key.Error ?? "unknown key");
if (cached != null && DateTime.UtcNow - cached.LastConfirmedUtc < grace)
{
var left = grace - (DateTime.UtcNow - cached.LastConfirmedUtc);
GameServer.Log($"WARNING: {problem} - starting with the license confirmed {LicenseCache.Span(DateTime.UtcNow - cached.LastConfirmedUtc)} ago " +
$"(owner {cached.Owner}). The server keeps running for {LicenseCache.Span(left)} unless the master confirms it again.");
owner = cached.Owner ?? "?";
confirmed = cached.LastConfirmedUtc;
}
else
{
// Nicht dauerhaft aufgeben: der Master kann gleich wieder da sein bzw. der Schlüssel wieder gelten
var retry = key.Unreachable ? MasterRetryDelay : LicenseRetryDelay;
startRequested = true;
startAt = DateTime.UtcNow + retry;
return Fail(problem + (key.Unreachable ? "" : " - manage your keys at " + MasterServer.Url + "/keys") +
$" - retrying in {LicenseCache.Span(retry)}");
}
}
if (!key.Valid)
return Fail("License key invalid: " + (key.Error ?? "unknown key") + " - manage your keys at " + MasterServer.Url + "/keys");
GameServer.Log($"License key OK ({key.Label}, owner {key.Owner})");
owner = key.Owner;
}
GameServer server;
@@ -251,6 +304,7 @@ public sealed class ServerHost
: "No save game yet - upload one as ZIP in the panel (World & Backups) or copy it to data/save");
server.Panel = Panel;
server.Host = this;
server.LicenseConfirmedUtc = confirmed;
ConfigureServer?.Invoke(server);
if (Config.Port != listenerPort) Listen(Config.Port); // Port in den Einstellungen geändert
return server;