License: running server keeps running until the next restart
The configurable LicenseGraceHours setting is gone. If the master is down or rejects the key, a running server now only warns and keeps running until the next restart; a (re)start still works up to 72 h after the last confirmation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,6 @@ public static class ConfigEditor
|
||||
requireSteam = config.RequireSteam,
|
||||
publicAddress = config.PublicAddress,
|
||||
saveSyncMinutes = config.SaveSyncMinutes,
|
||||
licenseGraceHours = config.LicenseGraceHours,
|
||||
autosaveSeconds = config.AutosaveSeconds,
|
||||
port = config.Port,
|
||||
panelEnabled = config.PanelEnabled,
|
||||
@@ -70,7 +69,6 @@ public static class ConfigEditor
|
||||
config.PublicAddress = Text("publicAddress", config.PublicAddress, 200);
|
||||
config.SaveSyncMinutes = Number("saveSyncMinutes", config.SaveSyncMinutes, 0, 240);
|
||||
config.AutosaveSeconds = Number("autosaveSeconds", config.AutosaveSeconds, 10, 3600);
|
||||
config.LicenseGraceHours = Number("licenseGraceHours", config.LicenseGraceHours, 1, 720);
|
||||
|
||||
int before = changed.Count;
|
||||
int oldPort = config.Port, oldPanelPort = config.PanelPort;
|
||||
|
||||
+5
-18
@@ -287,7 +287,6 @@ public sealed class GameServer
|
||||
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;
|
||||
@@ -1119,15 +1118,14 @@ public sealed class GameServer
|
||||
|
||||
/// <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>
|
||||
/// <summary>
|
||||
/// Antwort des Masters auf den Heartbeat auswerten. Ein laufender Server wird deswegen nie gestoppt - er läuft bis zum
|
||||
/// nächsten Neustart weiter; erst beim Start wird die Lizenz wieder geprüft (siehe ServerHost).
|
||||
/// </summary>
|
||||
void OnLicenseResult(int status)
|
||||
{
|
||||
if (status >= 200 && status < 300)
|
||||
@@ -1145,9 +1143,8 @@ public sealed class GameServer
|
||||
}
|
||||
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)}";
|
||||
$" (last confirmed {LicenseCache.Span(since)} ago) - the server keeps running until the next restart";
|
||||
if (DateTime.UtcNow >= nextLicenseWarning)
|
||||
{
|
||||
nextLicenseWarning = DateTime.UtcNow.AddMinutes(15);
|
||||
@@ -1155,16 +1152,6 @@ public sealed class GameServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
+4
-1
@@ -7,10 +7,13 @@ namespace MSCO.Server;
|
||||
/// <summary>
|
||||
/// Letzte erfolgreiche Lizenzprüfung beim Master (data/license.json). Ist der Master mal nicht erreichbar oder lehnt
|
||||
/// den Schlüssel kurz ab (z.B. nach einem Umzug ohne keys.json), läuft bzw. startet der Server damit weiter - bis
|
||||
/// LicenseGraceHours ohne Bestätigung vergangen sind. Gespeichert wird nur ein Hash des Schlüssels.
|
||||
/// GraceHours ohne Bestätigung vergangen sind (fest, nicht einstellbar). Gespeichert wird nur ein Hash des Schlüssels.
|
||||
/// </summary>
|
||||
public static class LicenseCache
|
||||
{
|
||||
/// <summary>So lange startet ein Server ohne Bestätigung des Masters noch (ein laufender läuft bis zum Neustart weiter).</summary>
|
||||
public static readonly TimeSpan Grace = TimeSpan.FromHours(72);
|
||||
|
||||
public sealed class Entry
|
||||
{
|
||||
public string KeyHash { get; set; }
|
||||
|
||||
+1
-3
@@ -401,9 +401,7 @@ function renderSettings(page) {
|
||||
h("div", { class: "section-head" }, h("h2", {}, "Saving")),
|
||||
h("div", { class: "section-body form-grid wide" },
|
||||
number("saveSyncMinutes", "Take the save game from a player every … minutes", c.saveSyncMinutes, { min: 0, max: 240, hint: "0 = only when someone joins" }),
|
||||
number("autosaveSeconds", "Write the world state every … seconds", c.autosaveSeconds, { min: 10, max: 3600 }),
|
||||
number("licenseGraceHours", "Keep running without license confirmation for … hours", c.licenseGraceHours, { min: 1, max: 720,
|
||||
hint: "If the master server is down or rejects the key, the server keeps running this long" }))),
|
||||
number("autosaveSeconds", "Write the world state every … seconds", c.autosaveSeconds, { min: 10, max: 3600 }))),
|
||||
h("div", { class: "section" },
|
||||
h("div", { class: "section-head" }, h("h2", {}, "Network & panel"), h("span", { class: "muted" }, "needs a restart")),
|
||||
h("div", { class: "section-body form-grid wide" },
|
||||
|
||||
+3
-5
@@ -283,7 +283,6 @@
|
||||
"Choose a ZIP, RAR or 7z file": "Wähle eine ZIP-, RAR- oder 7z-Datei",
|
||||
"License not confirmed": "Lizenz nicht bestätigt",
|
||||
"Keep running without license confirmation for … hours": "Ohne Lizenzbestätigung weiterlaufen für … Stunden",
|
||||
"If the master server is down or rejects the key, the server keeps running this long": "Ist der Master-Server aus oder lehnt den Schlüssel ab, läuft der Server so lange weiter",
|
||||
"⟳ Restart panel": "⟳ Panel neu starten",
|
||||
"Restart the panel?": "Panel neu starten?",
|
||||
"Applies the saved port and panel settings. If the game port changed, the game server restarts too. You stay logged in.":
|
||||
@@ -340,10 +339,9 @@
|
||||
[/^License key invalid: (.+)$/, "Lizenzschlüssel ungültig: $1"],
|
||||
[/^Master server not reachable (.+)$/, "Master-Server nicht erreichbar $1"],
|
||||
[/^Crashed: (.+)$/, "Abgestürzt: $1"],
|
||||
[/^The master server (rejects the license key|is not reachable) \(last confirmed (.+) ago\) - the server keeps running for (.+)$/,
|
||||
(m, why, ago, left) => (why.startsWith("rejects") ? "Der Master-Server lehnt den Lizenzschlüssel ab" : "Der Master-Server ist nicht erreichbar") +
|
||||
" (zuletzt bestätigt vor " + ago + ") - der Server läuft noch " + left + " weiter"],
|
||||
[/^The license key could not be confirmed by the master server for (.+)$/, "Der Lizenzschlüssel konnte $1 lang nicht vom Master-Server bestätigt werden"],
|
||||
[/^The master server (rejects the license key|is not reachable) \(last confirmed (.+) ago\) - the server keeps running until the next restart$/,
|
||||
(m, why, ago) => (why.startsWith("rejects") ? "Der Master-Server lehnt den Lizenzschlüssel ab" : "Der Master-Server ist nicht erreichbar") +
|
||||
" (zuletzt bestätigt vor " + ago + ") - der Server läuft bis zum nächsten Neustart weiter"],
|
||||
[/^This file could not be read - upload a ZIP, RAR or 7z archive (.+)$/, "Die Datei konnte nicht gelesen werden - lade ein ZIP-, RAR- oder 7z-Archiv hoch $1"],
|
||||
[/^No defaultES2File\.txt in the archive(.*)$/, "Keine defaultES2File.txt im Archiv - pack die Dateien aus %USERPROFILE%\\AppData\\LocalLow\\Amistech\\My Summer Car"],
|
||||
];
|
||||
|
||||
@@ -163,9 +163,9 @@ Mit diesem Code legst du im Browser das **Owner**-Konto an (erneut anzeigen: `ms
|
||||
- Oberfläche auf **Deutsch oder Englisch** (Umschalter unten in der Seitenleiste).
|
||||
- **Port oder Panel-Einstellungen geändert?** In den Einstellungen unten **„Panel neu starten“** – übernimmt sie sofort,
|
||||
man bleibt angemeldet und wird auf die neue Adresse weitergeleitet.
|
||||
- **Keymaster nicht erreichbar oder lehnt den Schlüssel ab?** Der Server läuft weiter und warnt im Log/Panel; erst nach
|
||||
`LicenseGraceHours` (Standard 72 h) ohne Bestätigung stoppt der Gameserver und startet von selbst wieder, sobald der
|
||||
Master den Schlüssel bestätigt. Auch ein Neustart klappt in dieser Zeit (letzte Bestätigung in `data/license.json`).
|
||||
- **Keymaster nicht erreichbar oder lehnt den Schlüssel ab?** Ein laufender Server läuft bis zum nächsten Neustart weiter
|
||||
und warnt im Log/Panel. Ein Neustart klappt noch bis 72 Stunden nach der letzten Bestätigung (gespeichert in
|
||||
`data/license.json`); danach startet der Server von selbst, sobald der Master den Schlüssel wieder bestätigt.
|
||||
- Passwörter werden nur als PBKDF2-Hash in `data/panel_users.json` gespeichert, Login mit Sperre nach
|
||||
Fehlversuchen, Sitzungs-Cookie (HttpOnly, SameSite=Strict), Schutz gegen CSRF.
|
||||
|
||||
|
||||
@@ -33,12 +33,6 @@ public sealed class ServerConfig
|
||||
/// <summary>Admins (SteamID64): dürfen im Spiel mit /admin Spieler kicken und bannen.</summary>
|
||||
public List<string> Admins { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// So lange (Stunden) läuft bzw. startet der Server weiter, wenn der Master den Lizenzschlüssel nicht bestätigen
|
||||
/// kann (nicht erreichbar oder lehnt ihn ab). Danach wird der Gameserver gestoppt, bis der Master ihn wieder bestätigt.
|
||||
/// </summary>
|
||||
public int LicenseGraceHours { get; set; } = 72;
|
||||
|
||||
// ---------------------------------------------------------------- Web-Panel
|
||||
/// <summary>Web-Panel einschalten.</summary>
|
||||
public bool PanelEnabled { get; set; } = true;
|
||||
|
||||
+2
-10
@@ -171,14 +171,6 @@ 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 ...");
|
||||
@@ -269,14 +261,14 @@ public sealed class ServerHost
|
||||
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 grace = LicenseCache.Grace;
|
||||
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}). The server keeps running until the next restart; a restart works for {LicenseCache.Span(left)} more.");
|
||||
owner = cached.Owner ?? "?";
|
||||
confirmed = cached.LastConfirmedUtc;
|
||||
}
|
||||
|
||||
+5
-9
@@ -486,7 +486,7 @@ async Task PanelTests()
|
||||
using (var da = new System.IO.Compression.ZipArchive(new MemoryStream(downZip)))
|
||||
Check(down.IsSuccessStatusCode && da.GetEntry("defaultES2File.txt") != null, "The current save can be downloaded as ZIP");
|
||||
|
||||
// Lizenz: Master fällt aus -> der Server läuft weiter, erst nach der Kulanzzeit wird er gestoppt
|
||||
// Lizenz: Master fällt aus -> der laufende Server läuft bis zum nächsten Neustart weiter
|
||||
LicenseCache.Save(dir, "test", "Tester", "Test", DateTime.UtcNow.AddHours(-5));
|
||||
var cachedLicense = LicenseCache.Load(dir, "test");
|
||||
Check(cachedLicense != null && cachedLicense.Owner == "Tester" && LicenseCache.Load(dir, "other-key") == null
|
||||
@@ -494,14 +494,10 @@ async Task PanelTests()
|
||||
"The last license check is remembered (only a hash of the key)");
|
||||
Srv().LicenseConfirmedUtc = DateTime.UtcNow.AddHours(-10); // innerhalb der 72 h: läuft weiter
|
||||
await Task.Delay(500);
|
||||
Check(host.State == HostState.Running, "Server keeps running while the license is unconfirmed but within the grace period");
|
||||
Srv().LicenseConfirmedUtc = DateTime.UtcNow.AddHours(-100); // Kulanzzeit abgelaufen
|
||||
await WaitForState(HostState.Error);
|
||||
Check(host.State == HostState.Error && (host.LastError ?? "").Contains("license"), "After the grace period the game server stops: " + host.LastError);
|
||||
Check((await Call(owner, "GET", "api/session")).status == 200, "The panel stays reachable after the license stop");
|
||||
Check((await Call(owner, "POST", "api/server/start")).status == 200, "It can be started again");
|
||||
await WaitForState(HostState.Running);
|
||||
Check(host.State == HostState.Running, "Running again after the start");
|
||||
Check(host.State == HostState.Running, "Server keeps running while the license is unconfirmed");
|
||||
Srv().LicenseConfirmedUtc = DateTime.UtcNow.AddHours(-100); // länger als die 72 h für einen Start
|
||||
await Task.Delay(1500);
|
||||
Check(host.State == HostState.Running, "A running server is never stopped by the license check - it runs until the next restart");
|
||||
|
||||
// Panel neu starten: eigener Port, danach wieder zurück auf den Spiel-Port - angemeldet bleiben
|
||||
Check((await Call(owner, "POST", "api/config", new { panelPort = 17791 })).status == 200, "Panel port can be changed");
|
||||
|
||||
Reference in New Issue
Block a user