Panel independent of the game server: ServerHost owns port + panel and starts/stops the game server; panel works while stopped/crashed/unconfigured (settings, save ZIP upload, backups, bans offline); crash auto-restart; players told 'offline'; tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 19:02:08 +02:00
co-authored by Claude Opus 5.5
parent f8aad82d18
commit c74c48dfc8
11 changed files with 1012 additions and 417 deletions
+15 -75
View File
@@ -60,9 +60,10 @@ if (fromEnvironment.Count > 0)
if (created && string.IsNullOrWhiteSpace(config.LicenseKey))
{
GameServer.Log("Created " + configPath);
GameServer.Log("Get a license key at " + MasterServer.Url + "/keys and put it into server.json (\"LicenseKey\").");
Pause();
return 1;
GameServer.Log("Get a license key at " + MasterServer.Url + "/keys" +
(config.PanelEnabled ? " and enter it in the web panel (Settings) - or put it into server.json (\"LicenseKey\")."
: " and put it into server.json (\"LicenseKey\")."));
if (!config.PanelEnabled) { Pause(); return 1; }
}
// --import-save [Pfad]: Spielstand aus My Summer Car übernehmen
@@ -94,12 +95,9 @@ if (Array.IndexOf(args, "--import-save") >= 0)
if (Directory.Exists(original)) Directory.Delete(original, true);
}
// Konsole, "msco <befehl>" (docker exec) und Signale gehen immer an den gerade laufenden Server -
// nach einem Neustart aus dem Panel ist das ein neuer. Das Web-Panel bleibt dabei bestehen (man bleibt angemeldet).
GameServer server = null;
AdminPanel panel = null;
Action<string> send = line => server?.EnqueueCommand(line);
Action stopRequested = () => { if (server == null) Environment.Exit(0); else server.EnqueueCommand("stop"); };
// Der Host läuft die ganze Zeit (Port + Web-Panel) und startet/stoppt den Gameserver - auch wenn der nicht starten
// kann (Lizenz, Spielstand ...), bleibt das Panel erreichbar. Konsole, "msco <befehl>" und Signale gehen an den Host.
var host = new ServerHost(config, dataDir, configPath);
// Konsolenbefehle in eigenem Thread lesen, ausgeführt werden sie in der Server-Schleife.
// Als Dienst (systemd/Docker ohne Terminal) gibt es keine Eingabe - dann einfach ohne Konsole laufen.
@@ -108,7 +106,7 @@ var input = new Thread(() =>
try
{
string line;
while ((line = Console.ReadLine()) != null) send(line);
while ((line = Console.ReadLine()) != null) host.Command(line);
}
catch { }
}) { IsBackground = true };
@@ -126,7 +124,7 @@ var commandWatcher = new Thread(() =>
if (!File.Exists(commandFile)) continue;
File.Move(commandFile, processing, true);
foreach (string line in File.ReadAllLines(processing))
if (line.Trim().Length > 0) { GameServer.Log("> " + line.Trim()); send(line.Trim()); }
if (line.Trim().Length > 0) { GameServer.Log("> " + line.Trim()); host.Command(line.Trim()); }
File.Delete(processing);
}
catch { }
@@ -135,71 +133,13 @@ var commandWatcher = new Thread(() =>
commandWatcher.Start();
// Sauber beenden (Weltzustand speichern) bei Strg+C, systemctl stop / docker stop (SIGTERM) und SIGQUIT
Console.CancelKeyPress += (_, e) => { e.Cancel = true; stopRequested(); };
using var sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => { ctx.Cancel = true; stopRequested(); });
using var sigquit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, ctx => { ctx.Cancel = true; stopRequested(); });
Console.CancelKeyPress += (_, e) => { e.Cancel = true; host.RequestExit(); };
using var sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => { ctx.Cancel = true; host.RequestExit(); });
using var sigquit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, ctx => { ctx.Cancel = true; host.RequestExit(); });
for (bool restarting = false; ; restarting = true)
{
if (restarting)
{
// Neustart: server.json neu lesen (im Panel geänderte Einstellungen), Lizenz und Spielstand neu prüfen
try { config = ServerConfig.LoadOrCreate(configPath, out _); }
catch (Exception e) { GameServer.Log("Cannot read " + configPath + " (" + e.Message + ") - keeping the previous settings"); }
config.ApplyEnvironment();
}
// Lizenzschlüssel beim Keymaster prüfen
if (string.IsNullOrWhiteSpace(config.LicenseKey))
{
GameServer.Log("No license key set. Get one at " + MasterServer.Url + "/keys and put it into server.json.");
Pause();
return 1;
}
// Automatisch mit dem Master (msco.mbm.wtf) verbinden: ist er gerade nicht erreichbar,
// wird es mit wachsendem Abstand (max. 60 s) weiter versucht, statt aufzugeben.
var master = new MasterClient(MasterServer.Url);
MasterClient.KeyInfo key;
for (int attempt = 1; ; attempt++)
{
GameServer.Log($"Checking license key at {MasterServer.Url} ...");
key = await master.ValidateKeyAsync(config.LicenseKey);
if (!key.Unreachable) break;
int wait = Math.Min(60, 5 * attempt);
GameServer.Log($"Master server not reachable ({key.Error}) - retrying in {wait} s (Ctrl+C to cancel)");
await Task.Delay(TimeSpan.FromSeconds(wait));
}
if (!key.Valid)
{
GameServer.Log("License key invalid: " + (key.Error ?? "unknown key"));
GameServer.Log("Manage your keys at " + MasterServer.Url + "/keys");
Pause();
return 1;
}
GameServer.Log($"License key OK ({key.Label}, owner {key.Owner})");
var next = new GameServer(config, dataDir, configPath, key.Owner);
if (!next.LoadSave())
{
Pause();
return 1;
}
// Web-Panel (wie txAdmin) - standardmäßig auf dem Spiel-Port; bei einem Neustart dasselbe Panel weiterverwenden
if (config.PanelEnabled || panel != null)
{
if (panel == null) panel = AdminPanel.Start(next, config, dataDir);
else panel.Attach(next, config);
next.Panel = panel;
}
server = next;
server.Run();
if (!server.RestartRequested) break;
server = null;
GameServer.Log("Restarting the server ...");
}
panel?.Stop();
return 0;
int exitCode = host.Run();
if (exitCode != 0) Pause();
return exitCode;
string ArgValue(string name)
{