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
+55 -7
View File
@@ -282,14 +282,11 @@ public sealed class GameServer
{
var result = heartbeat.Result;
heartbeat = null;
if (result.Status == 401 || result.Status == 403)
{
Log("License key was rejected by the master server - shutting down.");
running = false;
}
else if (result.Status == 0) Log("Master server not reachable - server is not listed right now.");
else ReportReachability(result);
OnLicenseResult(result.Status);
if (result.Status == 0 || result.Status >= 500) Log("Master server not reachable - server is not listed right now.");
else if (result.Status >= 200 && result.Status < 300) ReportReachability(result);
}
CheckLicenseGrace();
if (!offline && now >= nextHeartbeat && heartbeat == null)
{
nextHeartbeat = now + HeartbeatSeconds;
@@ -1067,6 +1064,7 @@ public sealed class GameServer
players = JoinedCount,
maxPlayers = config.MaxPlayers,
state = "running",
licenseWarning = LicenseWarning,
uptimeSeconds = (long)clock.Elapsed.TotalSeconds,
port = config.Port,
isPublic = config.Public,
@@ -1116,6 +1114,56 @@ public sealed class GameServer
public bool ExitRequested { get; private set; }
string stoppedBy; // gesetzt = gezielt gestoppt (Spieler bekommen den Grund statt "verbinde neu")
// ---------------------------------------------------------------- Lizenz: Kulanzzeit, wenn der Master ausfällt
/// <summary>Wann der Master den Schlüssel zuletzt bestätigt hat (setzt der Host beim Start).</summary>
public DateTime LicenseConfirmedUtc { get; set; } = DateTime.UtcNow;
/// <summary>Gesetzt, wenn der Server wegen fehlender Lizenzbestätigung gestoppt wurde.</summary>
public string LicenseStopReason { get; private set; }
/// <summary>Hinweis fürs Panel, solange der Master den Schlüssel nicht bestätigt (null = alles gut).</summary>
public string LicenseWarning { get; private set; }
DateTime nextLicenseWarning = DateTime.MinValue, lastLicenseCacheWrite = DateTime.MinValue;
TimeSpan LicenseGrace => TimeSpan.FromHours(Math.Clamp(config.LicenseGraceHours, 1, 24 * 30));
/// <summary>Antwort des Masters auf den Heartbeat auswerten - nie sofort stoppen.</summary>
void OnLicenseResult(int status)
{
if (status >= 200 && status < 300)
{
if (LicenseWarning != null) Log("The master server confirms the license key again - all good.");
LicenseWarning = null;
LicenseConfirmedUtc = DateTime.UtcNow;
if (DateTime.UtcNow - lastLicenseCacheWrite > TimeSpan.FromMinutes(10))
{
lastLicenseCacheWrite = DateTime.UtcNow;
var cached = LicenseCache.Load(DataDir, config.LicenseKey);
LicenseCache.Save(DataDir, config.LicenseKey, cached?.Owner ?? ownerName, cached?.Label, LicenseConfirmedUtc);
}
return;
}
bool rejected = status == 401 || status == 403;
var since = DateTime.UtcNow - LicenseConfirmedUtc;
var left = LicenseGrace - since;
LicenseWarning = (rejected ? "The master server rejects the license key" : "The master server is not reachable") +
$" (last confirmed {LicenseCache.Span(since)} ago) - the server keeps running for {LicenseCache.Span(left)}";
if (DateTime.UtcNow >= nextLicenseWarning)
{
nextLicenseWarning = DateTime.UtcNow.AddMinutes(15);
Log("WARNING: " + LicenseWarning + (rejected ? ". Check your key at " + MasterServer.Url + "/keys" : ""));
}
}
/// <summary>Kulanzzeit abgelaufen? Dann den Gameserver stoppen (das Panel bleibt, der Host versucht es weiter).</summary>
void CheckLicenseGrace()
{
if (LicenseStopReason != null || DateTime.UtcNow - LicenseConfirmedUtc <= LicenseGrace) return;
LicenseStopReason = $"The license key could not be confirmed by the master server for {LicenseCache.Span(LicenseGrace)}";
Log(LicenseStopReason + " - stopping the game server. It starts again automatically as soon as the master confirms the key.");
stoppedBy = "the license check";
running = false;
}
public void RequestExit()
{
ExitRequested = true;